pax_global_header00006660000000000000000000000064152572174350014525gustar00rootroot0000000000000052 comment=60d339d739c99a4e3ff533c25e16a1fb07840f17 pallets-eco-flask-security-72bf929/000077500000000000000000000000001525721743500172465ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/.djlintrc000066400000000000000000000004741525721743500210650ustar00rootroot00000000000000{ "ignore": "H005,H006,H017,H025,H030,H031", "extension": "html", "indent": "2", "profile": "jinja", "format_attribute_template_tags": "true", "max_line_length": 120, "max_attribute_length": 240, "blank_line_after_tag": "from,endmacro", "blank_line_before_tag": "block,extends" } pallets-eco-flask-security-72bf929/.editorconfig000066400000000000000000000011111525721743500217150ustar00rootroot00000000000000# -*- coding: utf-8 -*- root = true [*] indent_style = space end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true charset = utf-8 # Python files [*.py] indent_size = 4 # isort plugin configuration known_first_party = flask_security multi_line_output = 2 default_section = THIRDPARTY # RST files (used by sphinx) [*.rst] indent_size = 4 # CSS, HTML, JS, JSON, YML [*.{css,html,js,json,yml}] indent_size = 2 # Matches the exact files either package.json or .travis.yml [{package.json,.travis.yml}] indent_size = 2 # Dockerfile [Dockerfile] indent_size = 4 pallets-eco-flask-security-72bf929/.git-blame-ignore-revs000066400000000000000000000001471525721743500233500ustar00rootroot00000000000000# Black 721d31e9cb02af22e3ad9d579b7b82123527fafe # Black 2024 af6e7176eb54e7e9de77dd6e2cba03630c13f14e pallets-eco-flask-security-72bf929/.github/000077500000000000000000000000001525721743500206065ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/.github/workflows/000077500000000000000000000000001525721743500226435ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/.github/workflows/lock.yaml000066400000000000000000000013451525721743500244620ustar00rootroot00000000000000name: Lock inactive closed issues # Lock closed issues that have not received any further activity for two weeks. # This does not close open issues, only humans may do that. It is easier to # respond to new issues with fresh examples rather than continuing discussions # on old issues. on: schedule: - cron: '0 0 * * *' permissions: {} concurrency: group: lock cancel-in-progress: true jobs: lock: runs-on: ubuntu-latest permissions: issues: write pull-requests: write discussions: write steps: - uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6.0.2 with: issue-inactive-days: 14 pr-inactive-days: 14 discussion-inactive-days: 14 pallets-eco-flask-security-72bf929/.github/workflows/publish-too.yaml000066400000000000000000000040421525721743500257740ustar00rootroot00000000000000name: Publish-Too on: push: tags: - '*' jobs: build: runs-on: ubuntu-latest outputs: artifact-id: ${{ steps.upload-artifact.outputs.artifact-id }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version-file: pyproject-too.toml # Use the commit date instead of the current date during the build. - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV - name: Create dist run: | python -m pip install -U pip pip install tox tox -e makedist-too - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 id: upload-artifact with: name: dist path: dist/ if-no-files-found: error create-release: needs: [build] runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: artifact-ids: ${{ needs.build.outputs.artifact-id }} path: dist/ - name: create release run: gh release create --draft --repo ${{ github.repository }} ${{ github.ref_name }}-Too dist/* env: GH_TOKEN: ${{ github.token }} publish-pypi-too: needs: [build] # Wait for approval before attempting to upload to PyPI. This allows reviewing the # files in the draft release. environment: name: publish url: https://pypi.org/project/Flask-Security-Too/${{ github.ref_name }} runs-on: ubuntu-latest permissions: id-token: write steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: artifact-ids: ${{ needs.build.outputs.artifact-id }} path: dist/ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 with: packages-dir: dist/ pallets-eco-flask-security-72bf929/.github/workflows/publish.yaml000066400000000000000000000040601525721743500251750ustar00rootroot00000000000000name: Publish on: push: tags: - '*' jobs: build: runs-on: ubuntu-latest permissions: contents: read outputs: artifact-id: ${{ steps.upload-artifact.outputs.artifact-id }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version-file: pyproject.toml # Use the commit date instead of the current date during the build. - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV - name: Create dist run: | python -m pip install -U pip pip install tox tox -e makedist - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 id: upload-artifact with: name: dist path: dist/ if-no-files-found: error create-release: needs: [build] runs-on: ubuntu-latest permissions: contents: write steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: artifact-ids: ${{ needs.build.outputs.artifact-id }} path: dist/ - name: create release run: gh release create --draft --repo ${{ github.repository }} ${{ github.ref_name }} dist/* env: GH_TOKEN: ${{ github.token }} publish-pypi: needs: [build] # Wait for approval before attempting to upload to PyPI. This allows reviewing the # files in the draft release. environment: name: publish url: https://pypi.org/project/Flask-Security/${{ github.ref_name }} runs-on: ubuntu-latest permissions: id-token: write steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: artifact-ids: ${{ needs.build.outputs.artifact-id }} path: dist/ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 with: packages-dir: dist/ pallets-eco-flask-security-72bf929/.github/workflows/tests.yml000066400000000000000000000103361525721743500245330ustar00rootroot00000000000000 name: tests permissions: contents: read on: push: branches: - main - "[0-9]+.[0-9]+.x" pull_request: branches: - main - "[0-9]+.[0-9]+.x" # Allows you to run this workflow manually from the Actions tab workflow_dispatch: jobs: tests: runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - {python: '3.11', tox: 'py311-release'} - {python: '3.11', tox: 'py311-low'} - {python: '3.12', tox: 'py312-release' } - {python: '3.12', tox: 'py312-low' } - {python: '3.13', tox: 'py313-release' } - {python: '3.13', tox: 'py313-low' } - {python: '3.14', tox: 'py314-release' } steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python }} - name: update pip run: | python -m pip install -U pip - name: cache pip uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/pip key: pip-${{ runner.os }}-${{ matrix.tox }}-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('tox.ini') }} }} - name: run tests run: | pip install tox tox -e ${{ matrix.tox }} lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: update pip run: | python -m pip install -U pip - name: Style, docs, mypy run: | pip install tox tox -e style,docs,mypy other: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: update pip run: | python -m pip install -U pip - name: nobabel, nowebauthn, noauthlib, noflasksqlalchemy, async run: | pip install tox tox -e nobabel,nowebauthn,noauthlib,noflasksqlalchemy,async cov: runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: update pip run: | python -m pip install -U pip - name: Coverage run: | pip install tox coverage tox -e coverage - name: Upload coverage to Codecov uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 with: fail_ci_if_error: true verbose: true token: ${{ secrets.codecov_token }} realdb: runs-on: ubuntu-latest services: postgres: image: postgres:latest env: POSTGRES_USER: postgres POSTGRES_PASSWORD: testpw POSTGRES_DB: testdb ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.13" - name: update pip run: | python -m pip install -U pip - name: cache pip uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/pip key: pip-${{ runner.os }}-${{ hashFiles('pyproject.toml') }}-${{ hashFiles('tox.ini') }} }} - name: Postgres run: | pip install tox tox -e realpostgres -- --realdburl='postgresql://postgres:testpw@localhost:5432/testdb' pallets-eco-flask-security-72bf929/.gitignore000066400000000000000000000012601525721743500212350ustar00rootroot00000000000000*.py[co] # Packages *.egg *.egg-info dist *build eggs parts bin var sdist develop-eggs .installed.cfg pip-wheel-metadata/ __pycache__/ # Installer logs pip-log.txt # Unit test / coverage reports .coverage* .tox .travis*requirements* coverage.xml #Translations *.mo #Mr Developer .mr.developer.cfg #Virtualenv env/ venv/ .venv/ #Editor temporaries *~ *.swp *.save *.db *cache/ # vim [._]*.s[a-w][a-z] [._]s[a-w][a-z] *.un~ Session.vim .netrwhist *~ .eggs/README.txt # Mac .DS_Store # Linux .directory # Pycharm files .idea/ # VScode .vscode/ ### Emacs template # -*- mode: gitignore; -*- # *~ **/*~ \#*\# /.emacs.desktop /.emacs.desktop.lock *.elc auto-save-list tramp .\#* pallets-eco-flask-security-72bf929/.pre-commit-config.yaml000066400000000000000000000024411525721743500235300ustar00rootroot00000000000000# See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks default_language_version: python: python3.12 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # frozen: v6.0.0 hooks: - id: trailing-whitespace - id: debug-statements - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files - id: check-case-conflict - id: check-merge-conflict - id: fix-byte-order-marker - repo: https://github.com/asottile/pyupgrade rev: 75992aaa40730136014f34227e0135f63fc951b4 # frozen: v3.21.2 hooks: - id: pyupgrade args: [--py310-plus] - repo: https://github.com/psf/black rev: 87928e6d6761a4a6d22250e1fee5601b3998086e # frozen: 26.5.1 hooks: - id: black - repo: https://github.com/pycqa/flake8 rev: cfbbfdeb2391c14a37e101e92a77f54f329e1726 # frozen: 7.4.1 hooks: - id: flake8 additional_dependencies: - flake8-bugbear - flake8-implicit-str-concat - repo: https://github.com/Riverside-Healthcare/djLint rev: b8b8c45322b3f38e5933a8088991b0f0d8cd45c2 # frozen: v1.46.2 hooks: - id: djlint-jinja files: "\\.html" types_or: ['html'] pallets-eco-flask-security-72bf929/.readthedocs.yml000066400000000000000000000006671525721743500223450ustar00rootroot00000000000000 # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 build: os: ubuntu-24.04 tools: python: "3.13" jobs: install: - pip install --upgrade pip - pip install --group docs - pip install -e . # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py builder: html fail_on_warning: true pallets-eco-flask-security-72bf929/AUTHORS000066400000000000000000000014111525721743500203130ustar00rootroot00000000000000Flask-Security was written by Matt Wright and various contributors. Development Lead ```````````````` - Chris Wagner Maintainer `````````` - Chris Wagner Patches and Suggestions ``````````````````````` Alexander Sukharev Alexey Poryadin Andrew J. Camenga Anthony Plunkett Artem Andreev Catherine Wise Chris Haines Christophe Simonis David Ignacio Eric Butler Eskil Heyn Olsen Iuri de Silvio Jay Goel Jiri Kuncar Joe Esposito Joe Hand Josh Purvis Kostyantyn Leschenko Luca Invernizzi Manuel Ebert Martin Maillard PaweÅ‚ KrzeÅ›niak Robert Clark Rodrigue Cloutier Rotem Yaari Srijan Choudhary Tristan Escalada Vadim Kotov Walt Askew John Paraskevopoulos Chris Wagner Eric Regnier Gal Stainfeld Ivan Piskunov Tyler Baur Glenn Lehman pallets-eco-flask-security-72bf929/CHANGES.rst000066400000000000000000003076701525721743500210650ustar00rootroot00000000000000Flask-Security Changelog ======================== Here you can see the full list of changes between each Flask-Security release. Version 5.9.1 ------------- Released September 30, 2026 Fixes +++++ -(:pr:`1293`) Fix for open-redirect GHSA-ccgp-pv8r-95wm Version 5.9.0 ------------- Released September 24, 2026 This release adds support for refresh tokens (finally!). In addition there are several configuration changes that try to align Flask-Security with latest best-practices. Please read these notes carefully - in particular the change to the default auth token lifetime might severely impact some applications. Features & Improvements +++++++++++++++++++++++ - (:issue:`1206`) Add support for refresh tokens. See :ref:`token_topic` - (:pr:`1233`) Change :py:data:`SECURITY_TOKEN_MAX_AGE` from an int to a timedelta. Also - *change default from ``never expire`` to 15 minutes*. - (:pr:`1235`) Change default :py:data:`SECURITY_LOGOUT_METHODS` to be just ``"POST"`` - (:issue:`1228`) Change default ``csrf`` and ``tf_validity`` cookie config to ``secure=True`` - (:issue:`1228`) The ``tf_validity`` cookie name is now configurable via :py:data:`SECURITY_TWO_FACTOR_VALIDITY_COOKIE_NAME` - (:issue:`1237`) Add support for CSRF on logout (default ``False``) - (:pr:`1241`) Convert all _WITHIN configuration variables to use timedelta - (:issue:`1153`) Enable localization of %(within)s variables using humanize - (:pr:`1249`) Add link expiration to confirmation and reset password email templates. - (:issue:`536` Add template path configuration variables for all email templates. - (:issue:`1254`) Webauthn/passkey name input value is now sanitized and normalized. A new utility method :py:meth:`flask_security.input_svn` is now used and is available for applications to use. - (:pr:`1271`) Username validation and normalization now uses the new :py:meth:`flask_security.input_svn` utility. This has some backwards compatibility concerns - see below. - (:pr:`1259`) Allow redirects to exactly :py:data:`SECURITY_REDIRECT_BASE_DOMAIN` by adding '.' to :py:data:`SECURITY_REDIRECT_ALLOWED_SUBDOMAINS`. Fixes +++++ - (:issue:`1108`) ``/verify`` and ``/us-verify`` forms now include the optional ``next`` field so form-posted redirect URLs are validated and preserved. (DSeaStar) - (:issue:`1212`) Newly introduced :py:meth:`.UserMixin.is_locked` logic is inverted. - (:pr:`1234`) Fix for GHSA-f66q-9rf6-8795 - WebAuthn reauthentication freshness bypass. (tonghuaroot) - (:issue:`1244`) Fix login form remember me checkbox. - (:pr:`1258`) A JSON request body can set a form field (email, password, username, identity, name, phone, refresh token, recovery/2FA code, ...) to a non-string value (e.g. a dict), which used to crash with an unhandled ``TypeError`` instead of failing validation cleanly. (miettal) - (:issue:`1263`) :py:data:`SECURITY_CACHE_CONTROL` directives were added to every application response instead of just responses from Flask-Security endpoints as documented. - (:issue:`1272`) Properly re-export public methods (using redundant symbol aliases). Docs and Chores +++++++++++++++ - (:issue:`1208`) Remove support for Pony ORM - (:pr:`1240`) Remove deprecated get_token_status() and convert uses to check_and_get_token_status() - (:pr:`1252`) Replace ``bleach`` (deprecated/unmaintained) with ``nh3`` for username sanitization (tkfoss) - (:pr:`1273`) Improve translation tests to not use real french translations - (:pr:`1275`) Update Spanish and Catalan translations. (arielvb) - (:pr:`1283`) Improve (from 54% to 74%) pyright --verifytypes - (:pr:`1284`) Change dependencies to dependency-groups - (:pr:`1286`) Remove dependabot - (:pr:`1287`) Drop support for Python 3.10 Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ - The fix for the inverted `is_locked` logic will require any application using it to invert their logic. - The change to :py:data:`SECURITY_TOKEN_MAX_AGE` default improves a long-standing 'insecure-out-of-the-box' concern. Applications will have to either change the value if they really want a long-lasting token or use the new refresh token feature. - The default for :py:data:`SECURITY_LOGOUT_METHODS` has been changed to just allowing ``"POST"``. This is considered modern best practice. - The default configuration for :py:data:`SECURITY_TWO_FACTOR_VALIDITY_COOKIE` has been change to ``secure=True``. - The default configuration for :py:data:`SECURITY_CSRF_COOKIE` has been changed to ``secure=True`` - All _WITHIN configuration variables now take a timedelta instead of the home-grown ``<#> ``. The old form is still accepted, with a deprecation warning, and converted at flask-security app init time into a timedelta. - With the addition of email template path configuration variables, the :py:meth:`flask_security.send_mail` method's ``template`` parameter now requires a complete template path (relative to the application or blueprint root). - Username sanitization now uses ``nh3`` instead of ``bleach``. If you enabled :py:data:`SECURITY_USERNAME_ENABLE` you must now install ``nh3`` (included in the ``common`` extra) rather than ``bleach``. - Webauthn names are now sanitized, validated for certain allowable characters, and normalized. It is possible that some existing names could now be considered illegal and won't match (so can't be deleted via API). The allowed character categories can be set with :py:data:`SECURITY_WAN_NAME_ALLOWED_CHARS`. The module ``nh3`` is used to sanitize input - the application must have that package installed. - Username form input now uses the newer input sanitization and normalization utility class. The configuration variable ``SECURITY_USERNAME_NORMALIZE_FORM`` has been replaced with :py:data:`SECURITY_INPUT_NORMALIZE_FORM` (default is still "NKFD"). There is a new configuration option :py:data:`SECURITY_USERNAME_ALLOWED_CHARS` which can be used to change which unicode character classes are permitted in the username for validation (the default is ``["L", "N"]`` which is the same as before). The error message ``SECURITY_MSG_USERNAME_ILLEGAL_CHARACTERS`` has been replaced with ``SECURITY_MSG_INVALID_INPUT``. - The UsernameUtil.check_username method has been removed. Notes +++++ - The refresh token feature requires a new DB model - FsRefreshTracker - which must be added by the application. This model has been added to the `fsqla` and `sqla` all-inclusive models and their respective versions have been bumped. Version 5.8.2 ------------- Released August 11, 2026 Fixes +++++ - (:pr:`1265`) Fix for GHSA-f66q-9rf6-8795 - WebAuthn reauthentication freshness bypass. (tonghuaroot) - (:issue:`1263`) :py:data:`SECURITY_CACHE_CONTROL` directives were added to every application response instead of just responses from Flask-Security endpoints as documented. (Ceirced) - (:issue:`1212`) Newly introduced :py:meth:`.UserMixin.is_locked` logic is inverted. - (:issue:`1244`) Fix login form remember me checkbox. Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ - The fix for the inverted `is_locked` logic will require any application using it to invert their logic. Version 5.8.1 ------------- Released May 21, 2026 Fixes +++++ - (:issue:`1222`) Fix for GHSA-w2j7-f3c6-g8cw - Possible open-redirect with ALLOW_SUBDOMAIN option. - (:issue:`1215`) Fix for GHSA-97r5-pg8x-p63p - possible oauth bypass in /verify Version 5.8.0 ------------- Released April 15, 2026 Features & Improvements +++++++++++++++++++++++ - (:pr:`1170`) Add API :py:meth:`.UserMixin.check_tf_required` to allow applications to control which users require two-factor authentication. - (:issue:`1178`) Add Cache-Control headers. - (:issue:`1165`) Add support for using Social Login (OAuth) for verification. - (:issue:`1188`) Add tracking of failed authentication attempts via :py:meth:`.UserMixin.track_failed_authn` and signal :py:data:`user_failed_authn` - (:issue:`1192`) Add API for application to decide if a particular user account is locked. Fixes +++++ - (:issue:`1179`) Fix verify_password for bcrypt 5.0 (mephi42) - (:issue:`1200`) Fix username_recovery w.r.t. inactive and non-confirmed users - (:issue:`1189`) Return additional fields for JSON responses with QR codes Docs and Chores +++++++++++++++ - (:pr:`1150`) Update de_DE translations (swaeberle) - (:pr:`1151`) Update ca_ES translations (arielvb) - (:pr:`1152`) Update es_ES translations (arielvb) - (:pr:`1196`) Update arabic translations (samialfattani) - (:pr:`1199`) Update it_IT translations (gissimo) - (:pr:`1185`) Change external facing terminology from 'Social OAuth' to 'Social Login'. Version 5.7.1 ------------- Released November 23, 2025 Fixes +++++ - (:issue:`1147`) Regression when updating hash algorithm from bcrypt (willcroft) Version 5.7.0 ------------- Released November 14, 2025 This release contains a set of small backward incompatible changes. Please read these notes carefully. Features & Improvements +++++++++++++++++++++++ - (:pr:`1132`) Add Arabic translations (samialfattani) - (:issue:`1123`) Enable forgot-password workflow for authenticated users. Fixes +++++ - (:pr:`1115`) Fix broken link in docs and improve docstrings/typing for util classes. - (:issue:`1127`) Add nonce to script tags if configured to support nonce-based Content-Security-Policy (ahanak). - (:issue:`1133`) Remove unnecessary (optional) dependency on sqlalchemy_utils. - (:pr:`1140`) Fix localization of tf_select choices. - (:pr:`1143`) Support bcrypt 5.0 - See below for important compatibility concerns. This also replaces passlib with libpass for all versions. Docs and Chores +++++++++++++++ - (:pr:`1144`) Update ES and IT translations (gissimo) - (:pr:`1106`) Drop support for Python 3.9. This removes the dependency on importlib_resources, updates pypy to 3.10, and uses 3.12 as base python for tests/tox. - (:pr:`1112`) Flip :py:data:`SECURITY_USE_REGISTER_V2` default to ``True``. - (:pr:`1117`) Flip default mail package back to Flask-Mail (from Flask-Mailman). - (:issue:`1139`) Change external facing terminology from 'WebAuthn Credential' to 'passkey'. - (:pr:`1142`) Setting of xx_util_cls from kwargs which was deprecated in 5.6.1 has been removed. The BACKWARDS_COMPAT_UNAUTHN option (code) which has been deprecated since 5.4 has been removed. Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ - Flask-Security now depends on ``libpass`` (https://pypi.org/project/libpass/) for all versions. Be sure to UNINSTALL passlib, ensure the passlib directory is empty and then install libpass - we have seen reports when both are installed - it doesn't work! In bcrypt 5.0 they started throwing a ValueError for passwords/secrets longer than 72 bytes. It is important to know that by default Flask-Security performs a double hash - taking the secret, using HMAC(SHA512) then b64encodng the result. This means that ANY password will be longer than 72 bytes (86 to be exact). In the past bcrypt would silently truncate the input - now we have to do that explicitly. OWASP says truncation concerns are negligible: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#input-limits-of-bcrypt - The default RegisterForm is now the new RegisterFormV2 - Please read :ref:`register_form_migration`. Flask-Security will emit a DeprecationWarning if the :py:data:`SECURITY_USE_REGISTER_V2` is set to False. - In 5.0 we changed the default mailer package to Flask-Mailman since Flask-Mail was no longer supported. Flask-Mail is again supported and is part of Pallets-Eco. Both packages are still supported based on which one an application initializes. The only backwards compatibility concern is that if you use the setup extras 'common', it will install Flask-Mail rather than Flask-Mailman. - In the optional dependencies 'fsqla' we removed sqlalchemy_utils - while many applications might want these useful add-ons - they aren't required for standard SQLAlchemy use. Version 5.6.2 ------------- Released May 4, 2025 Fixes +++++ - (:issue:`1032` and :issue:`1096`) Use libpass for python >= 3.12 - (:pr:`1086`) Fix FR translation test for Change Password (nickcuenca) - (:issue:`1090`) Properly document context variables available in email templates. - (:issue:`1093`) Add confirmation link/token and reset link/token to welcome_existing email template. Notes +++++ Since Python 3.12 no longer contains setuptools - the old passlib failed to import. Rather than require setuptools, for Python >=3.12 we now depend on the fork ``libpass`` (https://pypi.org/project/libpass/) This is a very new package and rather than possibly cause backwards compat issues for projects not using Python >=3.12 - Flask-Security maintains the dependency on passlib for Python <3.12. Note: you can still use passlib for 3.12 and 3.13 - you have to manually add setuptools. Version 5.6.1 ------------- Released March 18, 2025 Fixes +++++ - (:issue:`1077`) Fix runtime modification of a config string (TWO_FACTOR_METHODS) - (:issue:`1078`) Fix CLI user_create when model doesn't contain username - (:issue:`1076`) xxx_util_cls instances should be public and documented. Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ As part of :issue:`1076` the following cleanup was done: - The xxx_util_cls arguments are now stored in 'private' instance variables - they are never used after Flask-Security initialization and have never been documented. - The xxx_util_cls options should only be set as part of Flask-Security construction. Setting them via init_app(kwargs) or app.config["SECURITY_XX"] has been deprecated. Version 5.6.0 ------------- Released February 12, 2025 Features & Improvements +++++++++++++++++++++++ - (:issue:`1038`) Add support for 'secret_key' rotation (jamesejr) - (:issue:`980`) Add support for username recovery in simple login flows (jamesejr) - (:issue:`1055`) Add support for changing username - (:pr:`1048`) Add support for Python 3.13 - (:issue:`1043`) Unify Register forms (and split out re-type password option) Please read :ref:`register_form_migration`. Fixes +++++ - (:pr:`1062`) Fix duplicate HTML ids in templates. - (:pr:`1067`) Fix more duplicate HTML ids in templates. - (:issue:`1064`) Ensure templates pass W3C validation (see below) Docs and Chores +++++++++++++++ - (:pr:`1052`) Remove deprecated TWO_FACTOR configuration variables - (:pr:`1069`) Update ES and IT translations (gissimo) - (:pr:`1071`) Improve templates - two-factor is hyphenated, re-authenticate is not. Also try to embed links into xlatable strings. Notes +++++ Python 3.13 removed ``crypt``, which passlib attempts to import and use as part of its safe_crypt() method (fallback is to return None). However - that method only appears to be called in a few crypt handlers and for bcrypt - only for the built-in bcrypt - not if the bcrypt package is installed. passlib is not maintained - a new fork (10/1/2024) (https://pypi.org/project/libpass/) seems promising and has been tested with python 3.13 and Flask-Security. If that fork matures we will change the dependencies appropriately. The register forms have been combined - or more accurately - there is a new RegisterFormV2 that subsumes the features of both the old RegisterForm and ConfirmRegisterForm. Please read :ref:`register_form_migration`. The SECURITY_TWO_FACTOR_{SECRET, URI_SERVICE_NAME, SMS_SERVICE, SMS_SERVICE_CONFIG} have been removed (they have been deprecated for a while). Use the equivalent :py:data:`SECURITY_TOTP_SECRETS`, :py:data:`SECURITY_TOTP_ISSUER`, :py:data:`SECURITY_SMS_SERVICE` and :py:data:`SECURITY_SMS_SERVICE_CONFIG`. Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ The fixes to all the templates to pass W3C validation could introduce some incompatibilities: - All templates now have a default - before, the <title> element was empty. - The HTML id of the rescue form submit button was changed to 'rescue' - The HTML id of the webauthn delete form name field was changed to 'delete-name' - Some template headings were changed to improve consistency - The csrf_token HTML id was changed on us_setup.html, wan_register.html, two_factor_setup.html two_factor_verify_code.html, us_verify.html, verify.html for the second form on the page. - On us_setup.html and two_factor_setup.html the submit code button HTML id was changed. Version 5.5.2 ------------- Released August 5, 2024 More attempts to upload to pypi both flask-security and flask-security-too. No code changes - however the build manifest changed so the source distribution contents might be slightly different. Docs and Chores +++++++++++++++ - (:pr:`1019`) Separate publish workflows for each pypi package Version 5.5.1 ------------- Released August 1, 2024 I am pleased to announce that Flask-Security-Too is now part of pallets-eco and has returned to be released as 'Flask-Security'. For the foreseeable future, we will publish the same release to both Flask-Security and Flask-Security-Too on PyPI. There are no code changes. Docs and Chores +++++++++++++++ - (:pr:`1015`) Convert docs, links, badges, etc to pallets-eco Version 5.5.0 ------------- Released July 24, 2024 Features & Improvements +++++++++++++++++++++++ - (:issue:`956`) Add support for changing registered user's email (:py:data:`SECURITY_CHANGE_EMAIL`). - (:issue:`944`) Change default password hash to argon2 (was bcrypt). See below for details. - (:pr:`990`) Add freshness capability to auth tokens (enables /us-setup to function w/ just auth tokens). - (:pr:`991`) Add support to /tf-setup to not require sessions (use a state token). - (:issue:`994`) Add support for Flask-SQLAlchemy-Lite - including new all-inclusive models that conform to sqlalchemy latest best-practice (type-annotated). - (:pr:`1007`) Convert other sqlalchemy-based datastores from legacy 'model.query' to best-practice 'select' - (:issue:`983`) Allow applications more flexibility defining allowable redirects. Fixes +++++ - (:pr:`972`) Set :py:data:`SECURITY_CSRF_COOKIE` at beginning (GET /login) of authentication ritual - just as we return the CSRF token. (thanks @e-goto) - (:issue:`973`) login and unified sign in should handle GET for authenticated user consistently. - (:pr:`995`) Don't show sms options if not defined in US_ENABLED_METHODS. (fredipevcin) - (:pr:`1009`) Change :py:data:`SECURITY_DEPRECATED_HASHING_SCHEMES` to ``["auto"]``. Docs and Chores +++++++++++++++ - (:pr:`979`) Update Russian translations (ademaro) - (:pr:`1004`) Update ES and IT translations (gissimo) - (:pr:`981` and :pr:`977`) Improve docs - (:pr:`992`) The long deprecated `get_token_status` is no longer exported - (:pr:`992`) Drop Python 3.8 support. - (:issue:`1001`) Try a different approach to typing User and Role models. Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ - Notes around the change to argon2 as the default password hash: - applications should add the argon2_cffi package to their requirements (it is included in the flask_security[common] extras). - leave bcrypt installed so that old passwords still work. - the default configuration will re-hash passwords with argon2 upon first use. - Changes to /tf-setup The old path - using state set in the session still works as before. The new path is just for the case an authenticated user wants to change their 2FA setup. - Changes to sqlalchemy-based datastores Flask-Security no longer uses the legacy model.query - all DB access is done via `select(xx).where(xx)`. As a result the find_user() method now only takes a SINGLE column:value from its kwargs - in prior releases all kwargs were passed into the query.filter. Version 5.4.3 ------------- Released March 23, 2024 Fixes +++++ - (:issue:`950`) Regression - some templates no longer getting correct config (thanks pete7863). - (:issue:`954`) CSRF not properly ignored for application forms using :py:data:`SECURITY_CSRF_PROTECT_MECHANISMS`. - (:pr:`957`) Improve jp translations (e-goto) - (:issue:`959`) Regression - datetime_factory should still be an attribute (thanks TimotheeJeannin) - (:issue:`942`) :py:data:`SECURITY_RETURN_GENERIC_RESPONSES` hid email validation/syntax errors. Version 5.4.2 ------------- Released March 8, 2024 Fixes +++++ - (:issue:`946`) OpenAPI spec missing. - (:pr:`945`) Doc fixes (e-goto) - (:pr:`941`) Update ES/IT translations (gissimo) Version 5.4.0 & 5.4.1 ---------------------- Released February 26, 2024 Among other changes, this continues the process of dis-entangling Flask-Security from Flask-Login and may require some application changes due to backwards incompatible changes. Features & Improvements +++++++++++++++++++++++ - (:issue:`879`) Work with Flask[async]. view decorators and signals support async handlers. - (:pr:`900`) CI support for python 3.12 - (:pr:`901`) Work with py_webauthn 2.0 (and only 2.0+) - (:pr:`899`) Improve (and simplify) Two-Factor setup. See below for backwards compatability issues and new functionality. - (:issue:`912`) Improve oauth debugging support. Handle next propagation in a more general way. - (:pr:`877`) Make AnonymousUser (Flask-Login) optional and deprecated. - (:pr:`906`) Remove undocumented and untested looking in session for possible 'next' redirect location. - (:pr:`881`) No longer rely on Flask-Login.unauthorized callback. See below for implications. - (:issue:`904`) Changes to default unauthorized handler - remove use of referrer header (see below) and document precise behavior. - (:pr:`927`) The authentication_token format has changed - adding per-token expiry time and future session ID. Old tokens are still accepted. Docs and Chores +++++++++++++++ - (:pr:`889`) Improve method translations for unified signin and two factor. Remove support for Flask-Babelex. - (:pr:`911`) Chore - stop setting all config as attributes. init_app(\*\*kwargs) can only set forms, flags, and utility classes (see below for compatibility concerns). - (:pr:`873`) Update Spanish and Italian translations. (gissimo) - (:pr:`855`) Improve translations for two-factor method selection. (gissimo) - (:pr:`866`) Improve German translations. (sr-verde) - (:pr:`911`) Remove deprecation of AUTO_LOGIN_AFTER_CONFIRM - it has a reasonable use case. - (:pr:`931`) Update message extraction - note that the CONFIRM_REGISTRATION message was changed to improve readability. Fixes +++++ - (:issue:`845`) us-signin magic link should use fs_uniquifier (not email). - (:issue:`893`) Improve open-redirect vulnerability mitigation. (see below) - (:issue:`875`) user_datastore.create_user has side effects on mutable inputs. (NoRePercussions) - (:pr:`878`) The long deprecated _unauthorized_callback/handler has been removed. - (:issue:`884`) Oauth re-used POST_LOGIN_VIEW which caused confusion. See below for the new configuration and implications. - (:pr:`908`) Improve CSRF documentation and testing. Fix bug where a CSRF failure could return an HTML page even if the request was JSON. - (:issue:`925`) Register with JSON and authentication token failed CSRF. (lilz-egoto) - (:issue:`870`) Fix 2 issues with CSRF configuration. - (:pr:`914`) It was possible that if :data:`SECURITY_EMAIL_VALIDATOR_ARGS` were set that deliverability would be checked even for login. Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ - Passing in an AnonymousUser class as part of Security initialization has been removed. - The never-public method _get_unauthorized_response has been removed. - Social-Oauth - a new configuration variable :py:data:`SECURITY_POST_OAUTH_LOGIN_VIEW` was introduced and it replaces :py:data:`SECURITY_POST_LOGIN_VIEW` in the oauthresponse logic when :py:data:`SECURITY_REDIRECT_BEHAVIOR` == `"spa"`. - Two-Factor setup. Prior to this release when setting up "SMS" the `/tf-setup` endpoint could be POSTed to w/o a phone number, and then another POST could be made to set the phone number. This has always been confusing and added complexity to the code. Now, if "SMS" is selected, the phone number must be supplied (which has always been supported). Other changes: - The default two-factor-setup.html template now has a more generic `"Enter code to complete setup"` message. - Make sure the `"disable"` option first. - Adding any currently configured two-factor method on setup failure. - The two_factor_verify template won't show the rescue form if it isn't set. - A GET on /tf-validate now returns the two-factor-validate-form always - before if the client was validating a new method, it would return the two-factor-setup-form - After successfully disabling two-factor the client is redirected to :py:data:`SECURITY_TWO_FACTOR_POST_SETUP_VIEW` rather than :py:data:`SECURITY_POST_LOGIN_VIEW`. - Bring unauthenticated handling completely into Flask-Security: Prior to this release, Flask-Security's :meth:`.Security.unauthn_handler` - called when a request wasn't properly authenticated - handled JSON requests then delegated form responses to Flask-Login's unauthenticated_callback. That logic has been moved into Flask-Security and Flask-Login is configured to call back into Flask-Security's handler. While the logic is very similar the following differences might be observed: - Flask-Login's FORCE_HOST_FOR_REDIRECTS configuration isn't honored - Flask-Login's USE_SESSION_FOR_NEXT configuration isn't honored - The flashed message is SECURITY_MSG_UNAUTHENTICATED rather than SECURITY_MSG_LOGIN. Furthermore, SECURITY_MSG_UNAUTHENTICATED was reworded to read better. - Flask-Login uses `urlencode` to encode the `next` query param - which quotes the '/' character. Werkzeug (which Flask-Security uses to build the URL) uses `quote` which considers '/' a safe character and isn't encoded. - The signal sent on an unauthenticated request has changed to :data:`user_unauthenticated`. Flask-Login used to send a `user_unauthorized` signal. - Flask-Security no longer configures anything related to Flask-Login's `fresh_login` logic. This shouldn't be used - instead use Flask-Security's :meth:`flask_security.auth_required` decorator. - Support for Flask-Babelex has been removed. Please convert to Flask-Babel. - JSON error response has changed due to issue with WTForms form-level errors. When WTForms introduced form-level errors they added it to the form.errors response using `None` as a key. When serializing it, it would turn into "null". However, if there is more than one error the default settings for JSON serialization in Flask attempt to sort the keys - which fails with the `None` key. An issue has been filed with WTForms - and maybe it will be changed. Flask-Security now changes any `None` key to `""`. - The default unauthorized handler behavior has changed slightly and is now documented. The default (:data:`SECURITY_UNAUTHORIZED_VIEW` == ``None``) has not changed (a default HTTP 403 response). The precise behavior when :data:`SECURITY_UNAUTHORIZED_VIEW` is set was never documented. The important change is that Flask-Security no longer ever looks at the request.referrer header and will never redirect to it. If an application needs that, it can provide a callable that can return that or any other header. - Configuration variables (and other things) are no longer added as attributes on the Security instance. For example `security.username_enable` no longer exists - this could be an issue in code or templates. For templates, Flask places `config` in the Jinja context - so rather than using an attribute, use `config["SECURITY_USERNAME_ENABLE"]` for the example above. - Open Redirect mitigation. Release 4.1.0 had a fix for :issue:`486` involving a potential open redirect. This was very low priority since the default configuration of Werkzeug (always convert the Location header to absolute URL) rendered the vulnerability un-exploitable. The solution at that time was to add an optional regex looking for these bizarre URLs that from a HTTP spec perspective are relative, but various browsers would interpret as absolute. In Werkzeug release 2.1 the default was changed so that the Location header was allowed to be a relative URL. This made the open redirect vulnerability much more likely to be exploitable. More recently, additional bizarre URLs were found, as documented in :issue:`893`. More work was done and a patch release 5.3.3 was published. This fix utilized changing the Werkzeug default back to absolute and an updated regex. Comments and thoughts by @gmanfuncky proposed a much better solution and that is in 5.4. This implementation is independent of Werkzeug (and relative Location headers are again the default). The entire regex option has been removed. Instead, any user-supplied path used as a redirect is parsed and quoted. Notes ++++++ - Historically, the **current_user** proxy (managed by Flask-Login) always pointed to a user object. If the user wasn't authenticated, it pointed to an AnonymousUser object. With this release, setting :py:data:`SECURITY_ANONYMOUS_USER_DISABLED` to `True` will force **current_user** to be set to `None` if the requesting user isn't authenticated. It should be noted that this is in support of a proposal by the Pallets team to remove AnonymousUser from Flask-Login - as well as deprecating the `is_authenticated` property. The default behavior (`False`) should be the same as prior releases. A new function `_fs_is_user_authenticated` is now part of the render_template context that templates can use instead of `current_user.is_authenticated`. Version 5.3.3 ------------- Released December 29, 2023 Fixes +++++ - (:issue:`893`) Once again work on open-redirect vulnerability - this time due to newer Werkzeug. Addresses: CVE-2023-49438 Version 5.3.2 ------------- Released October 23, 2023 Fixes ++++++ - (:issue:`859`) Update Quickstart to show how to properly handle SQLAlchemy connections. - (:issue:`861`) Auth Token not returned from /tf-validate. (thanks lilz-egoto) - (:pr:`864`) Fix for latest email_validator deprecation - bump minimum to 2.0.0 - (:pr:`865`) Deprecate passing in the anonymous_user class (sent to Flask-Login). Version 5.3.1 ------------- Released October 14, 2023 **Please Note:** - If your application uses webauthn you must use pydantic < 2.0 until the issue with user_handle is resolved. Fixes ++++++ - (:issue:`847`) Compatability with Flask 3.0 (wangsha) - (:issue:`829`) Revert change in 5.3.0 that added a Referrer-Policy header. - (:issue:`826`) Fix error in quickstart (codycollier) - (:pr:`835`) Update Armenian translations (amkrtchyan-tmp) - (:pr:`831`) Update German translations. (sr-verde) - (:issue:`853`) Fix 'next' propagation when passed as form.next (thanks cariaso) Version 5.3.0 ------------- Released July 27, 2023 This is a minor version bump due to some small backwards incompatible changes to WebAuthn, recoverability (/reset), confirmation (/confirm) and the two factor validity feature. Fixes ++++++ - (:pr:`807`) Webauthn Updates to handling of transport. - (:pr:`809`) Fix MongoDB support by eliminating dependency on flask-mongoengine. Improve MongoDB quickstart. - (:issue:`801`) Fix Quickstart for SQLAlchemy with scoped session. - (:issue:`806`) Login no longer, by default, checks for email deliverability. - (:issue:`791`) Token authentication is no longer accepted on endpoints which only allow 'session' as authentication-method. (N247S) - (:issue:`814`) /reset and /confirm and GENERIC_RESPONSES and additional form args don't mix. - (:issue:`281`) Reset password can be exploited and other OWASP improvements. - (:pr:`817`) Confirmation can be exploited and other OWASP improvements. - (:pr:`819`) Convert to pyproject.toml, build, remove setup.py/.cfg. - (:pr:`823`) the tf_validity feature now ONLY sets a cookie - and the token is no longer returned as part of a JSON response. - (:pr:`825`) Fix login/unified signin templates to properly send CSRF token. Add more tests. - (:pr:`826`) Improve Social Oauth example code. Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ - To align with the W3C WebAuthn Level2 and 3 spec - transports are now part of the registration response. This has been changed BOTH in the server code (using webauthn data structures) as well as the sample JavaScript code. If an application has their own JavaScript front end code - it might need to be changed. - The tf_validity feature :py:data:`SECURITY_TWO_FACTOR_ALWAYS_VALIDATE` used to set a cookie if the request was form based, and return the token as part of a JSON response. Now, this feature is ONLY cookie based and the token is no longer returned as part of any response. - Reset password was changed to adhere to OWASP recommendations and reduce possible exploitation: - A new email (with new token) is no longer sent upon expired token. Users must restart the reset password process. - The user is no longer automatically logged in upon successful password reset. For backwards compatibility :py:data:`SECURITY_AUTO_LOGIN_AFTER_RESET` can be set to ``True``. Note that this compatibility feature is deprecated and will be removed in a future release. - Identity information (identity, email) is no longer sent as part of the URL redirect query params. - The SECURITY_MSG_PASSWORD_RESET_EXPIRED message no longer contains the user's identity/email. - The default for :py:data:`SECURITY_RESET_PASSWORD_WITHIN` has been changed from `5 days` to `1 days`. - The response to GET /reset/<token> sets the HTTP header `Referrer-Policy` to `no-referrer` as suggested by OWASP. *PLEASE NOTE: this was backed out in 5.3.1* - Confirm email was changed to adhere to OWASP recommendations and reduce possible exploitation: - A new email (with new token) is no longer sent upon expired token. Users must restart the confirmation process. - Identity information (identity, email) is no longer sent as part of the URL redirect query params. - The :py:data:`SECURITY_AUTO_LOGIN_AFTER_CONFIRM` configuration variable now defaults to ``False`` - meaning after a successful email confirmation, the user must still sign in using the usual mechanisms. This is to align better with OWASP best practices. Setting it to ``True`` will restore prior behavior. - The SECURITY_MSG_CONFIRMATION_EXPIRED message no longer contains the user's identity/email. - The response to GET /reset/<token> sets the HTTP header `Referrer-Policy` to `no-referrer` as suggested by OWASP. *PLEASE NOTE: this was backed out in 5.3.1* Version 5.2.0 ------------- Released May 6, 2023 Note: Due to rapid deprecation and removal of APIs from the Pallets team, maintaining the testing of back versions of various packages is taking too much time and effort. In this release only current versions of the various dependent packages are being tested. Fixes +++++ - (:issue:`764`) Remove old Werkzeug compatibility check. - (:issue:`777`) Compatibility with Quart. - (:pr:`780`) Remove dependence on pkg_resources / setuptools (use importlib_resources package) - (:pr:`792`) Fix tests to work with latest Werkzeug/Flask. Update requirements_low to match current releases. - (:pr:`792`) Drop support for Python 3.7 Known Issues ++++++++++++ - Flask-mongoengine hasn't released in a while and currently will not work with latest Flask and Flask-Security-Too/Flask-Security (this is due to the JSONEncoder being deprecated and removed). Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ - The removal of pkg_resources required changing the config variable :py:data:`SECURITY_I18N_DIRNAME`. If your application modified or extended this configuration variable, a small change will be required. Version 5.1.2 ------------- Released March 12, 2023 Fixes +++++ - (:issue:`771`) Hungarian translations not working. - (:pr:`769`) Fix documentation for send_mail. (gg) - (:pr:`768`) Fix for latest mongoengine and mongomock. - (:pr:`766`) Fix inappropriate use of &thinsp& in French translations. (maxdup) - (:pr:`773`) Improve documentation around subclassing forms. Version 5.1.1 ------------- Released March 1, 2023 Fixes +++++ - (:issue:`740`) Fix 2 Flask apps in same thread with USERNAME_ENABLE set. There was a too aggressive config check. - (:pr:`739`) Update Russian translations. (ademaro) - (:pr:`743`) Run all templates through a linter. (ademaro) - (:pr:`757`) Fix json/flask backwards compatibility hack. - (:issue:`759`) Fix quickstarts - make sure they run using `flask run` - (:pr:`755`) Fix unified signup when two-factor not enabled. (sebdroid) - (:pr:`763`) Add dependency on setuptools (pkg_resources). (hroncok) Version 5.1.0 ------------- Released January 23, 2023 Features ++++++++ - (:issue:`667`) Expose form instantiation. See :ref:`form_instantiation`. - (:issue:`693`) Option to encrypt recovery codes. - (:pr:`716`) Support for authentication via 'social' oauth. - (:pr:`721`) Support for Python 3.11 Fixes +++++ - (:pr:`678`) Fixes for Flask-SQLAlchemy 3.0.0. (jrast) - (:pr:`680`) Fixes for sqlalchemy 2.0.0 (jrast) - (:issue:`697`) Webauthn and Unified signin features now properly take into account blueprint prefixes. - (:issue:`699`) Properly propagate `?next=/xx` - the verify, webauthn, and unified signin endpoints, that had multiple redirects, needed fixes. - (:pr:`696`) Add Hungarian translations. (xQwexx) - (:issue:`701`) Two factor redirects ignored url_prefix. Added a :py:data:`SECURITY_TWO_FACTOR_ERROR_VIEW` configuration option. - (:issue:`704`) Add configurations for static folder/URL and make sure templates reference blueprint relative static folder. - (:issue:`709`) Make (some) templates look better by using single quotes instead of double quotes. - (:issue:`690`) Send entire context to MailUtil::send_mail (patrickyan) - (:pr:`728`) Support for Flask-Babel 3.0.0 - (:issue:`692`) Add configuration option :py:data:`SECURITY_TWO_FACTOR_POST_SETUP_VIEW` which is redirected to upon successful change of a two factor method. - (:pr:`733`) The ability to pass in a LoginManager instance which was deprecated in 5.0 has been removed. - (:issue:`732`) If :py:data:`SECURITY_USERNAME_REQUIRED` was ``True`` then users couldn't login with just an email. - (:issue:`734`) If :py:data:`SECURITY_USERNAME_ENABLE` is set, bleach is a requirement. - (:pr:`736`) The unauthz_handler now takes a function name, not the function! Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ - Each form class used to be set as an attribute on the Security object. With the new form instantiation model, they no longer are. - After a successful update/change of a two-factor method, the user was redirected to :py:data:`SECURITY_POST_LOGIN_VIEW`. Now it redirects to :py:data:`SECURITY_TWO_FACTOR_POST_SETUP_VIEW` which defaults to `".two_factor_setup"`. - The :meth:`.Security.unauthz_handler` now takes a function name - not the function - which never made sense. Version 5.0.2 ------------- Released September 23, 2022 Fixes +++++ - (:issue:`673`) Role permissions backwards compatibility bug. For SQL based datastores that use Flask-Security's models.fsqla_vx - there should be NO issues. If you declare your own models - please see the 5.0.0 releases notes for required change. Version 5.0.1 ------------- Released September 6, 2022 Fixes +++++ - (:pr:`662`) Fix Change Password regression. (tysonholub) Version 5.0.0 ------------- Released August 27, 2022 **PLEASE READ CHANGE NOTES CAREFULLY - THERE ARE LIKELY REQUIRED CHANGES YOU WILL HAVE TO MAKE.** Features ++++++++ - (:issue:`475`) Support for WebAuthn. - (:issue:`479`) Support Two-factor recovery codes. - (:issue:`585`) Provide option to prevent user enumeration (i.e. Generic Responses). - (:pr:`532`) Support for Python 3.10. - (:pr:`657`, :pr:`655`) Support for Flask >= 2.2. - (:pr:`540`) Improve Templates in support of JS required by WebAuthn. - (:pr:`608`) Add Icelandic translations. (ofurkusi) - (:pr:`650`) Update German translations. (sr-verde) - (:issue:`256`) Add custom HTML attributes to improve user experience. This changed LoginForm quite a bit - please see backwards compatability concerns below. The default LoginForm and template should be the same as before. - (:pr:`638`) The JSON errors response has been unified. Please see backwards compatibility concerns below. - Updated all-inclusive data models (fsqla_v3). Add fields necessary for the new WebAuthn and Two-Factor recovery codes features. Changed `us_phone_number` to be unique (but not required). Changed `password` to be nullable. Deprecations ++++++++++++ - (:pr:`568`) Deprecate the old passwordless feature in favor of Unified Signin. - (:pr:`568`) Deprecate replacing login_manager so we can possibly vendor that in in the future. - (:pr:`654`) The previously deprecated methods RoleMixin.add_permissions and RoleMixin.remove_permissions have been removed. - (:pr:`657`) The ability to pass in a json_encoder_cls as part of initialization has been removed since Flask 2.2 has deprecated and replaced that functionality. - (:pr:`655`) Flask has deprecated @before_first_request. This was used mostly in examples/quickstart. These have been changed to use app.app_context() prior to running the app. Flask-Security itself used it in 2 places - to populate `_` in jinja globals if Babel wasn't initialized and to perform various configuration sanity checks w.r.t. WTF CSRF. All Flask-Security templates have been converted to use `_fsdomain` rather than ``_`` so Flask-Security no longer sets ``_`` into jinja2 globals. The configuration checks have been moved to the end of Security::init_app() - so it is now imperative that `FlaskWTF::CSRFProtect()` be called PRIOR to initializing Flask-Security. - encrypt_password method has been removed. It has been deprecated since 2.0.2 - get_token_status has been deprecated. Fixes +++++ - (:pr:`591`) Make the required zxcvbn complexity score configurable. (mephi42) - (:issue:`531`) Get rid of Flask-Mail. Flask-Mailman is now the default preferred email package. Flask-Mail is still supported so there should be no backwards compatability issues. - (:issue:`597`) A delete option has been added to us-setup (form and view). - (:pr:`625`) Improve username support - the LoginForm now has a separate field for username if ``SECURITY_USERNAME_ENABLE`` is True, and properly displays input fields only if the associated field is an identity attribute (as specified by :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES`). - (:pr:`627`) Improve empty password handling. Prior, an unguessable password was set into the user record when a user registered without a password - now, the DB user model has been changed to allow nullable passwords. This provides a better user experience since Flask-Security now knows if a user has an empty password or not. Since registering without a password is not a mainstream feature, a new configuration variable :py:data:`SECURITY_PASSWORD_REQUIRED` has been added (defaults to ``True``). - (:issue:`479`) A new configuration option :py:data:`SECURITY_TWO_FACTOR_RESCUE_EMAIL` has been added that allows disabling that feature - defaults to backwards compatible ``True`` - (:issue:`658`) us_phone_number needs to be validated to be unique. Backward Compatibility Concerns ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ For unified signin: - The redirect after a successful us-setup used to redirect to ``SECURITY_US_POST_SETUP_VIEW`` or ``SECURITY_POST_LOGIN_VIEW`` (which would default to '/'). Now it just redirects to ``SECURITY_US_POST_SETUP_VIEW`` which defaults back to the ``/us-setup`` view. - The ability to authenticate using a one-time email link was automatically setup by the system for all users. "email" now behaves like the other unified sign in methods and must be explicitly set up - with the exception that if a user registers WITHOUT a password, the system will setup the one-time email link option - since otherwise the user would never be able to authenticate. - ``/us-signin/send-code`` didn't used to check if the user account required confirmation it just sent a code and the ``/us-signin`` endpoint did the confirmation check. Now ``send-code`` does the confirmation check and won't send a code unless the user is confirmed. - In ``us-verify`` the 'code_methods' item now lists just active/setup methods that generate a code not ALL possible methods that generate a code. - ``SECURITY_US_VERIFY_SEND_CODE_URL`` and ``SECURITY_US_SIGNIN_SEND_CODE_URL`` endpoints are now POST only. - Empty passwords were always permitted when ``SECURITY_UNIFIED_SIGNIN`` was enabled - now an additional configuration variable ``SECURITY_PASSWORD_REQUIRED`` must be set to False. - ``SECURITY_US_VERIFY_SEND_CODE_URL`` and ``SECURITY_US_SIGNIN_SEND_CODE_URL`` used to send ``code_sent`` to the template. Now they flash the ``SECURITY_MSG_CODE_HAS_BEEN_SENT`` message. - With the addition of being able to delete a previously setup up sign in method, the signal `us_profile_changed` arguments have changed. `method` is now `methods` and is a list, and a new argument `delete` is True if a sign in option was deleted. Login: - Since the beginning of time, the flask-security login form has accepted any input in the 'email' field, and used that to check if it corresponds to any field in ``SECURITY_USER_IDENTITY_ATTRIBUTES``. This has always been problematic and confusing - and with the addition of HTML attributes for various form fields - having a field with multiple possible inputs is no longer a viable user experience. This is no longer supported, and the LoginForm now declares the ``email`` field to be of type ``EmailField`` which requires a valid (after normalization) email address. The most common usage of this legacy feature was to allow an email or username - Flask-Security now has core support for a ``username`` option - see :py:data:`SECURITY_USERNAME_ENABLE`. Please see :ref:`custom_login_form` for an example of how to replicate the legacy behavior. - Some error messages have changed - ``USER_DOES_NOT_EXIST`` is now returned for any identity error including an empty value. Other: - A very old piece of code in registrable, would immediately commit to the DB when a new user was created. It is now consistent with all other views, and has the caller responsible for committing the transaction - usually by setting up a flask ``after_this_request`` action. This could affect an application that captured the registration signal and stored the ``user`` object for later use - this user object would likely be invalid after the request is finished. - Some fields have custom HTML attributes attached to them (e.g. autocomplete, type, etc). These are stored as part of the form in the ``render_kw`` attribute. This could cause some confusion if an app had its own templates and set different attributes. - The keys for "/tf-rescue" select options have changed to be more 'action' oriented: - `lost_device` -> `email` - `no_mail_access` -> `help` - JSON error responses. **THIS IS A BREAKING CHANGE**. In earlier releases, the JSON error response could have either a `error` key which was for rare cases where there was a single non-form related error, or an `errors` key which was a a dict as defined by WTForms. Now, the `errors` key will contain a list of (localized) messages - both non-form related as well as any form related. The key `field_errors` will contain the dict as specified by WTForms. Please note that starting with WTForms 3.0 form-level errors are supported and show up in the dict with the field name/key of "none". There are no changes to non-error related JSON responses. - Permissions **THIS IS A BREAKING CHANGE**. The Role Model now stores permissions as a list, and requires that the underlying DB ORM map that to a supported DB type. For SQLAlchemy, this is mapped to a comma separated string (as before). For SQLAlchemy DBs the underlying Column type (UnicodeText) didn't change so no data migration should be required. However, the ORM Column type did change and requires the following change to your model:: from flask_security import AsaList from sqlalchemy.ext.mutable import MutableList class Role(Base, RoleMixin): ... permissions = Column(MutableList.as_mutable(AsaList()), nullable=True) ... If your application makes use of Flask-Security's models.fsqla_vX classes - no changes are required. For Mongo, a ListField can be directly used. - CSRF - As mentioned above, it is now required that `FlaskWTF::CSRFProtect()`, if used, must be called PRIOR to initializing Flask-Security. - json_encoder_cls - As mentioned above - Flask-Security initialization no longer accepts overriding the json_encoder class. If this is required, update to Flask >=2.2 and implement Flask's JSONProvider interface. For templates: - Pretty much every template was modified to replace <p> with <div class=xx> to make styling possible and to make more complex forms more readable. - Many forms had places where things weren't properly localizable - that has (hopefully) been fixed. - The ``us_setup.html`` template was modified to add ability to delete an existing set up method. DB Migration ~~~~~~~~~~~~ To use the new WebAuthn feature a new table and two new columns in the User model are required. To ease updates - Flask-Security will automatically create a fs_webauthn_user_handle upon first use for existing users. If you are using Alembic the schema migration is easy:: op.add_column('user', sa.Column('fs_webauthn_user_handle', sa.String(length=64), nullable=True, unique=True)) If you want to allow for empty passwords as part of registration then set :py:data:`SECURITY_PASSWORD_REQUIRED` to ``False``. In addition you need to change your DB schema to allow the ``password`` field to be nullable. Version 4.1.5 ------------- Released July 28, 2022 Fixes +++++ - (:pr:`644`) Fix test and other failures with newer Flask-Login/Werkzeug versions. Version 4.1.4 ------------- Released April 19, 2022 Fixes +++++ - (:issue:`594`) Fix test failures with newer Flask versions. Version 4.1.3 ------------- Released March 2, 2022 Fixes +++++ - (:issue:`581`) Fix bug when attempting to disable register_blueprint. (halali) - (:pr:`539`) Fix example documentation re: generating localized messages. (kazuhei2) - (:pr:`546`) Make roles joinedload compatible with SQLAlchemy 2.0. (keats) - (:pr:`586`) Ship py.typed as part of package. - (:issue:`580`) Improve documentation around use of bleach and include in common install extra. Version 4.1.2 ------------- Released September 22, 2021 Fixes +++++ - (:issue:`526`) default_reauthn_handler doesn't honor SECURITY_URL_PREFIX - (:pr:`528`) Improve German translations (sr-verde) - (:pr:`527`) Fix two-factor sample code (djpnewton) Version 4.1.1 -------------- Released September 10, 2021 Fixes +++++ - (:issue:`518`) Fix corner case where Security object was being reused in tests. - (:issue:`512`) If USERNAME_ENABLE is set, change LoginForm field from EmailField to StringField. Also - dynamically add fields to Login and Registration forms rather than always having them - this made the RegistrationForm much simpler. - (:issue:`516`) Improved username feature handling solved issue of always requiring bleach. - (:issue:`513`) Improve documentation of default username validation. Version 4.1.0 ------------- Released July 23, 2021 Features ++++++++ - (:issue:`474`) Add public API and CLI command to change a user's password. - (:issue:`140`) Add type hints. Please note that many of the packages that flask-security depends on aren't typed yet - so there are likely errors in some of the types. - (:issue:`466`) Add first-class support for using username for signing in. Fixes +++++ - (:issue:`483`) 4.0 doesn't accept 3.4 authentication tokens. (kuba-lilz) - (:issue:`490`) Flask-Mail sender name can be a tuple. (hrishikeshrt) - (:issue:`486`) Possible open redirect vulnerability. - (:pr:`478`) Improve/update German translation. (sr-verde) - (:issue:`488`) Improve handling of Babel packages. - (:pr:`496`) Documentation improvements, distribution extras, fix single message override. - (:issue:`497`) Improve cookie handling and default ``samesite`` to ``Strict``. Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ - (:pr:`488`) In 4.0.0, with the addition of Flask-Babel support, Flask-Security enforced that if it could import either Flask-Babel or Flask-BabelEx, that those modules had been initialized as proper Flask extensions. Prior to 4.0.0, just Flask-BabelEx was supported - and that didn't require any explicit initialization. Flask-Babel DOES require explicit initialization. However for some applications that don't completely control their environment (such as system pre-installed versions of python) this caused applications that didn't even want translation services to fail on startup. With this release, Flask-Security still attempts to import one or the other package - however if those modules are NOT initialized, Flask-Security will simply ignore them and no translations will occur. - (:issue:`497`) The CSRF_COOKIE and TWO_FACTOR_VALIDITY cookie had their defaults changed to set ``samesite=Strict``. This follows the Flask-Security goal of making things more secure out-of-the-box. - (:issue:`140`) Type hinting. For the most part this of course has no runtime effects. However, this required a fairly major overhaul of how Flask-Security is initialized in order to provide valid types for the many constructor attributes. There are no known compatability concerns - however initialization used to convert all arguments into kwargs then add those as attributes and merge with application constants. That no longer happens and it is possible that some corner cases don't behave precisely as they did before. Version 4.0.1 ------------- Released April 2, 2021 Features ++++++++ Fixes +++++ - (:issue:`461`) 4.0 doesn't accept 3.4 authentication tokens. (kuba-lilz) - (:issue:`460`) 2-fa error: Failed to send code - improved documentation and debuggability. - (:issue:`454`) 2-fa error: TypeError - fixed documentation. - (:issue:`443`) Calling create user without any arguments - fixed underlying cause of translating form errors in the CLI. - (:issue:`442`) Email validation confusion - added documentation. - (:issue:`450`) Add documentation on how to override specific error messages. - (:pr:`439`) Don't install global-scope tests. (mgorny) - (:pr:`470`) Add note about updating DB using MySQL. (jugmac00) - (:pr:`468`) Fix documentation - uia_phone_number should be uia_phone_mapper. (dvrg) - (:pr:`457`) Improve chinese translations. (zxjlm) - (:pr:`453`) Improve basque and spanish translations. (mmozos) - (:pr:`448`) Add Afrikaans translations. (lonelyvikingmichael) - (:pr:`467`) Add Blinker as explicit dependency, improve/fix celery usage docs, dont require pyqrcode unless authenticator configured, improve SMS configuration variables documentation. Version 4.0.0 ------------- Released January 26, 2021 **PLEASE READ CHANGE NOTES CAREFULLY - THERE ARE LIKELY REQUIRED CHANGES YOU WILL HAVE TO MAKE TO EVEN START YOUR APPLICATION WITH 4.0** Start Here +++++++++++ - Your UserModel must contain ``fs_uniquifier`` - Either uninstall Flask-BabelEx (if you don't need translations) or add either Flask-Babel (>=2.0) or Flask-BabelEx to your dependencies AND be sure to initialize it in your app. - Add Flask-Mail to your dependencies. - If you have unicode emails or passwords read change notes below. Version 4.0.0rc2 ---------------- Released January 18, 2021 Features & Cleanup +++++++++++++++++++ - Removal of python 2.7 and <3.6 support - Removal of token caching feature (a relatively new feature that had some systemic issues) - (:pr:`328`) Remove dependence on Flask-Mail and refactor. - (:pr:`335`) Remove two-factor `/tf-confirm` endpoint and use generic `freshness` mechanism. - (:pr:`336`) Remove ``SECURITY_BACKWARDS_COMPAT_AUTH_TOKEN_INVALID(ATE)``. In addition to not making sense - the documentation has never been correct. - (:pr:`339`) Require ``fs_uniquifier`` in the UserModel and stop using/referencing the UserModel primary key. - (:pr:`349`) Change ``SECURITY_USER_IDENTITY_ATTRIBUTES`` configuration variable semantics. - Remove (all?) requirements around having an 'email' column in the UserModel. API change - JSON SPA redirects used to always include a query param 'email=xx'. While that is still sent (if and only if) the UserModel contains an 'email' columns, a new query param 'identity' is returned which returns the value of :meth:`.UserMixin.calc_username()`. - (:pr:`382`) Improvements and documentation for two-factor authentication. - (:pr:`394`) Add support for email validation and normalization (see :class:`.MailUtil`). - (:issue:`231`) Normalize unicode passwords (see :class:`.PasswordUtil`). - (:issue:`391`) Option to redirect to `/confirm` if user hits an endpoint that requires confirmation. New option :py:data:`SECURITY_REQUIRES_CONFIRMATION_ERROR_VIEW` which if set and the user hits the `/login`, `/reset`, or `/us-signin` endpoint, and they require confirmation the response will be a redirect. (SnaKyEyeS) - (:issue:`366`) Allow redirects on sub-domains. Please see :py:data:`SECURITY_REDIRECT_ALLOW_SUBDOMAINS`. (willcroft) - (:pr:`376`) Have POST redirects default to Flask's ``APPLICATION_ROOT``. Previously the default configuration was ``/``. Now it first looks at Flask's `APPLICATION_ROOT` configuration and uses that (which also by default is ``/``. (tysonholub) - (:pr:`401`) Add 2FA Validity Window so an application can configure how often the second factor has to be entered. (baurt) - (:pr:`403`) Add HTML5 Email input types to email fields. This has some backwards compatibility concerns outlined below. (drola) - (:pr:`413`) Add hy_AM translations. (rudolfamirjanyan) - (:pr:`410`) Add Basque and fix Spanish translations. (mmozos) - (:pr:`408`) Polish translations. (kamil559) - (:pr:`390`) Update ru_RU translations. (TitaniumHocker) Fixed +++++ - (:issue:`389`) Fixes for translations. First - email subjects were never being translated. Second, converted all templates to use _fsdomain(xx) rather than _(xx) so that they get translated regardless of the app's domain. - (:issue:`381`) Support Flask-Babel 2.0 which has backported Domain support. Flask-Security now supports Flask-Babel (>=2.00), Flask-BabelEx, as well as no translation support. Please see backwards compatibility notes below. - (:pr:`352`) Fix issue with adding/deleting permissions - all mutating methods must be at the datastore layer so that db.put() can be called. Added :meth:`.UserDatastore.add_permissions_to_role` and :meth:`.UserDatastore.remove_permissions_from_role`. The methods `.RoleMixin.add_permissions` and `.RoleMixin.remove_permissions` have been deprecated. - (:issue:`395`) Provide ability to change table names for User and Role tables in the fsqla model. - (:issue:`338`) All sessions are invalidated when a user changes or resets their password. This is accomplished by changing the user's `fs_uniquifier`. The user is automatically re-logged in (and a new session created) after a successful change operation. - (:issue:`418`) Two-factor (and to a lesser extent unified sign in) QRcode fetching wasn't protected via CSRF. The fix makes things secure and simpler (always good); however read below for compatibility concerns. In addition, the elements that make up the QRcode (key, username, issuer) area also made available to the form and returned as part of the JSON return value - this allows for manual or other ways to initialize the authenticator app. - (:issue:`421`) GET on `/login` and `/change` could return the callers authentication_token. This is a security concern since GETs don't have CSRF protection. This bug was introduced in 3.3.0. Backwards Compatibility Concerns +++++++++++++++++++++++++++++++++ - (:pr:`328`) Remove dependence on Flask-Mail and refactor. The ``send_mail_task`` and ``send_mail`` methods as part of Flask-Security initialization have been removed and replaced with a new :class:`.MailUtil` class. The utility method :func:`.send_mail` can still be used. If your application didn't use either of the deprecated methods, then the only change required is to add Flask-Mail to your package requirements (since Flask-Security no longer lists it). Please see the :ref:`emails_topic` for updated examples. - (:pr:`335`) Convert two-factor setup flow to use the freshness feature rather than its own verify password endpoint. This COMPLETELY removes the ``/tf-confirm`` endpoint and associated form: ``two_factor_verify_password_form``. Now, when /tf-setup is invoked, the :meth:`flask_security.check_and_update_authn_fresh` is invoked, and if the current session isn't 'fresh' the caller will be redirected to a verify endpoint (either :py:data:`SECURITY_VERIFY_URL` or :py:data:`SECURITY_US_VERIFY_URL`). The simplest change would be to call ``/verify`` everywhere the application used to call ``/tf-confirm``. - (:pr:`339`) Require ``fs_uniquifier``. In 3.3 the ``fs_uniquifier`` was added in the UserModel to fix the slow authentication token issue. In 3.4 the ``fs_uniquifier`` was used to implement Flask-Login's `Alternative Token` feature - thus decoupling the primary key (id) from any security context. All along, there have been a few issues with applications not wanting to use the name 'id' in their model, or wanting a different type for their primary key. With this change, Flask-Security no longer interprets or uses the UserModel primary key - just the ``fs_uniquifier`` field. See the changes section for 3.3 for information on how to do the schema and data upgrades required to add this field. There is also an API change - the JSON response (via UserModel.get_security_payload()) returned the ``user.id`` field. With this change the default is an empty directory - override :meth:`.UserMixin.get_security_payload()` to return any portion of the UserModel you need. - (:pr:`349`) :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES` has changed syntax and semantics. It now contains the combined information from the old ``SECURITY_USER_IDENTITY_ATTRIBUTES`` and the newly introduced in 3.4 :py:data:`SECURITY_USER_IDENTITY_MAPPINGS`. This enabled changing the underlying way we validate credentials in the login form and unified sign in form. In prior releases we simply tried to look up the form value as the PK of the UserModel - this often failed and then looped through the other ``SECURITY_USER_IDENTITY_ATTRIBUTES``. This had a history of issues, including many applications not wanting to have a standard PK for the user model. Now, using the mapping configuration, the UserModel attribute/column the input corresponds to is determined, then the UserModel is queried specifically for that *attribute:value* pair. If you application didn't change the variable, no modifications are required. - (:pr:`354`) The :class:`flask_security.PhoneUtil` is now initialized as part of Flask-Security initialization rather than ``@app.before_first_request`` (since that broke the CLI). Since it isn't called in an application context, the *app* being initialized is passed as an argument to *__init__*. - (:issue:`381`) When using Flask-Babel (>= 2.0) it is required that the application initialize Flask-Babel (e.g. Babel(app)). Flask-BabelEx would self-initialize so it didn't matter. Flask-Security will throw a run time error upon first request if Flask-Babel OR FLask-BabelEx is installed, but not initialized. Also, Flask-Security no longer has a dependency on either Flask-Babel or Flask-BabelEx - if neither are installed, it falls back to a dummy translation. *If your application expects translation services, it must specify the appropriate* *dependency AND initialize it.* - (:pr:`394`) Email input is now normalized prior to being stored in the DB. Previously, it was validated, but the raw input was stored. Normalization and validation rely on the `email_validator <https://pypi.org/project/email-validator/>`_ package. The :class:`.MailUtil` class provides the interface for normalization and validation - allowing all this to be customized. If you have unicode local or domain parts - existing users may have difficulties logging in. Administratively you need to read each user record, normalize the email (see :class:`.MailUtil`), and write it back. - (:issue:`381`) Passwords are now, by default, normalized using Python's unicodedata.normalize() method. The :py:data:`SECURITY_PASSWORD_NORMALIZE_FORM` defaults to "NKFD". This brings Flask-Security in line with the NIST recommendations outlined in `Memorized Secret Verifiers <https://pages.nist.gov/800-63-3/sp800-63b.html#sec5>`_ If your users have unicode passwords they may have difficulty authenticating. You can turn off this normalization or have your users reset their passwords. Password normalization and validation has been encapsulated in a new :class:`.PasswordUtil` class. This replaces the method ``password_validator`` introduced in 3.4.0. - (:pr:`403`) By default all forms that have an email as input now use the wtforms html5 ``EmailField``. For most applications this will make the user experience slightly nicer - especially for mobile devices. Some applications use the email form field for other identity attributes (such as username). If your application does this you will probably need to subclass ``LoginForm`` and change the email type back to StringField. - (:issue:`338`) By default, both passwords and authentication tokens use the same attribute ``fs_uniquifier`` to uniquely identify the user. This means that if the user changes or resets their password, all authentication tokens also become invalid. This could be viewed as a feature or a bug. If this behavior isn't desired, add another uniquifier: ``fs_token_uniquifier`` to your UserModel and that will be used to generate authentication tokens. - (:issue:`418`) Fix CSRF vulnerability w.r.t. getting QRcodes. Both two-factor and unified-signup had a separate GET endpoint to fetch the QRcode when setting up an authenticator app. GETS don't have any CSRF protection. Both of those endpoints have been completely removed, and the QRcode is embedded in a successful POST of the setup form. The changes to the templates are minimal and of course if you didn't override the template - there is no compatibility concern. - (:issue:`421`) Fix CSRF vulnerability on `/login` and `/change` that could return the callers authentication token. Now, callers can only get the authentication token on successful POST calls. Version 3.4.5 -------------- Released January 8, 2021 Security Vulnerability Fix. Two CSRF vulnerabilities were reported: `qrcode`_ and `login`_. This release fixes the more severe of the 2 - the `/login` vulnerability. The QRcode issue has a much smaller risk profile since a) it is only for two-factor authentication using an authenticator app b) the qrcode is only available during the time the user is first setting up their authentication app. The QRcode issue has been fixed in 4.0. .. _qrcode: https://github.com/pallets-eco/flask-security/issues/418 .. _login: https://github.com/pallets-eco/flask-security/issues/421 Fixed +++++ - (:issue:`421`) GET on `/login` and `/change` could return the callers authentication_token. This is a security concern since GETs don't have CSRF protection. This bug was introduced in 3.3.0. Backwards Compatibility Concerns ++++++++++++++++++++++++++++++++ - (:issue:`421`) Fix CSRF vulnerability on `/login` and `/change` that could return the callers authentication token. Now, callers can only get the authentication token on successful POST calls. Version 3.4.4 -------------- Released July 27, 2020 Bug/regression fixes. Fixed +++++ - (:issue:`359`) Basic Auth broken. When the unauthenticated handler was changed to provide a more uniform/consistent response - it broke using Basic Auth from a browser, since it always redirected rather than returning 401. Now, if the response headers contain ``WWW-Authenticate`` (which is set if ``basic`` @auth_required method is used), a 401 is returned. See below for backwards compatibility concerns. - (:pr:`362`) As part of figuring out issue 359 - a redirect loop was found. In release 3.3.0 code was put in to redirect to :py:data:`SECURITY_POST_LOGIN_VIEW` when GET or POST was called and the caller was already authenticated. The method used would honor the request ``next`` query parameter. This could cause redirect loops. The pre-3.3.0 behavior of redirecting to :py:data:`SECURITY_POST_LOGIN_VIEW` and ignoring the ``next`` parameter has been restored. - (:issue:`347`) Fix peewee. Turns out - due to lack of unit tests - peewee hasn't worked since 'permissions' were added in 3.3. Furthermore, changes in 3.4 around get_id and alternative tokens also didn't work since peewee defines its own `get_id` method. Compatibility Concerns ++++++++++++++++++++++ In 3.3.0, :meth:`flask_security.auth_required` was changed to add a default argument if none was given. The default include all current methods - ``session``, ``token``, and ``basic``. However ``basic`` really isn't like the others and requires that we send back a ``WWW-Authenticate`` header if authentication fails (and return a 401 and not redirect). ``basic`` has been removed from the default set and must once again be explicitly requested. Version 3.4.3 ------------- Released June 12, 2020 Minor fixes for a regression and a couple other minor changes Fixed +++++ - (:issue:`340`) Fix regression where tf_phone_number was required, even if SMS wasn't configured. - (:pr:`342`) Pick up some small documentation fixes from 4.0.0. Version 3.4.2 ------------- Released May 2, 2020 Only change is to move repo to the Flask-Middleware github organization. Version 3.4.1 -------------- Released April 22, 2020 Fix a bunch of bugs in new unified sign in along with a couple other major issues. Fixed +++++ - (:issue:`298`) Alternative ID feature ran afoul of postgres/psycopg2 finickiness. - (:issue:`300`) JSON 401 responses had WWW-Authenticate Header attached - that caused browsers to pop up their own login/password form. Not what applications want. - (:issue:`280`) Allow admin/api to setup TFA (and unified sign in) out of band. Please see :meth:`.UserDatastore.tf_set`, :meth:`.UserDatastore.tf_reset`, :meth:`.UserDatastore.us_set`, :meth:`.UserDatastore.us_reset` and :meth:`.UserDatastore.reset_user_access`. - (:pr:`305`) We used form._errors which wasn't very pythonic, and it was removed in WTForms 2.3.0. - (:pr:`310`) WTForms 2.3.0 made email_validator optional - we need it. Version 3.4.0 ------------- Released March 31, 2020 Features ++++++++ - (:pr:`257`) Support a unified sign in feature. Please see :ref:`configuration:unified signin`. - (:pr:`265`) Add phone number validation class. This is used in both unified sign in as well as two-factor when using ``sms``. - (:pr:`274`) Add support for 'freshness' of caller's authentication. This permits endpoints to be additionally protected by ensuring a recent authentication. - (:issue:`99`, :issue:`195`) Support pluggable password validators. Provide a default validator that offers complexity and breached support. - (:issue:`266`) Provide interface to two-factor send_token so that applications can provide error mitigation. Defaults to returning errors if can't send the verification code. - (:pr:`247`) Updated all-inclusive data models (fsqlaV2). Add fields necessary for the new unified sign in feature and changed 'username' to be unique (but not required). - (:pr:`245`) Use fs_uniquifier as the default Flask-Login 'alternative token'. Basically this means that changing the fs_uniquifier will cause outstanding auth tokens, session and remember me cookies to be invalidated. So if an account gets compromised, an admin can easily stop access. Prior to this cookies were storing the 'id' which is the user's primary key - difficult to change! (kishi85) Fixed +++++ - (:issue:`273`) Don't allow reset password for accounts that are disabled. - (:issue:`282`) Add configuration that disallows GET for logout. Allowing GET can cause some denial of service issues. The default still allows GET for backwards compatibility. (kantorii) - (:issue:`258`) Reset password wasn't integrated into the two-factor feature and therefore two-factor auth could be bypassed. - (:issue:`254`) Allow lists and sets as underlying permissions. (pffs) - (:issue:`251`) Allow a registration form to have additional fields that aren't part of the user model that are just passed to the user_registered.send signal, where the application can perform arbitrary additional actions required during registration. (kuba-lilz) - (:issue:`249`) Add configuration to disable the 'role-joining' optimization for SQLAlchemy. (pffs) - (:issue:`238`) Fix more issues with atomically setting the new TOTP secret when setting up two-factor. (kishi85) - (:pr:`240`) Fix Quart Compatibility. (ristellise) - (:issue:`232`) CSRF Cookie not being set when using 'Remember Me' cookie to re-sign in. (kishi85) - (:issue:`229`) Two-factor enabled accounts didn't work with the Remember Me feature. (kishi85) As part of adding unified sign in, there were many similarities with two-factor. Some refactoring was done to unify naming, configuration variables etc. It should all be backwards compatible. - In TWO_FACTOR_ENABLED_METHODS "mail" was changed to "email". "mail" will still be honored if already stored in DB. Also "google_authenticator" is now just "authenticator". - TWO_FACTOR_SECRET, TWO_FACTOR_URI_SERVICE_NAME, TWO_FACTOR_SMS_SERVICE, and TWO_FACTOR_SMS_SERVICE_CONFIG have all been deprecated in favor of names that are the same for two-factor and unified sign in. Other changes with possible backwards compatibility issues: - ``/tf-setup`` never did any phone number validation. Now it does. - ``two_factor_setup.html`` template - the chosen_method check was changed to ``email``. If you have your own custom template - be sure make that change. Version 3.3.3 ------------- Released February 11, 2020 Minor changes required to work with latest released Werkzeug and Flask-Login. Version 3.3.2 ------------- Released December 7, 2019 - (:issue:`215`) Fixed 2FA totp secret regeneration bug (kishi85) - (:issue:`172`) Fixed 'next' redirect error in login view - (:issue:`221`) Fixed regressions in login view when already authenticated user again does a GET or POST. - (:issue:`219`) Added example code for unit testing FS protected routes. - (:issue:`223`) Integrated two-factor auth into registration and confirmation. Thanks to kuba-lilz and kishi85 for finding and providing detailed issue reports. In Flask-Security 3.3.0 the login view was changed to allow already authenticated users to access the view. Prior to 3.3.0, the login view was protected with @anonymous_user_required - so any access (via GET or POST) would simply redirect the user to the ``POST_LOGIN_VIEW``. With the 3.3.0 changes, both GET and POST behaved oddly. GET simply returned the login template, and POST attempted to log out the current user, and log in the new user. This was problematic since this couldn't possibly work with CSRF. The old behavior has been restored, with the subtle change that older Flask-Security releases did not look at "next" in the form or request for the redirect, and now, all redirects from the login view will honor "next". Version 3.3.1 ------------- Released November 16, 2019 - (:pr:`197`) Add `Quart <https://gitlab.com/pgjones/quart/>`_ compatibility (Ristellise) - (:pr:`194`) Add Python 3.8 support into CI (jdevera) - (:pr:`196`) Improve docs around Single Page Applications and React (acidjunk) - (:issue:`201`) fsqla model was added to __init__.py making Sqlalchemy a required package. That is wrong and has been removed. Applications must now explicitly import from ``flask_security.models`` - (:pr:`204`) Fix/improve examples and quickstart to show one MUST call hash_password() when creating users programmatically. Also show real SECRET_KEYs and PASSWORD_SALTs and how to generate them. - (:pr:`209`) Add argon2 as an allowable password hash. - (:pr:`210`) Improve integration with Flask-Admin. Actually - this PR improves localization support by adding a method ``_fsdomain`` to jinja2's global environment. Added documentation around localization. Version 3.3.0 ------------- Released September 26, 2019 **There are several default behavior changes that might break existing applications. Most have configuration variables that restore prior behavior**. **If you use Authentication Tokens (rather than session cookies) you MUST make a (small) change. Please see below for details.** - (:pr:`120`) Native support for Permissions as part of Roles. Endpoints can be protected via permissions that are evaluated based on role(s) that the user has. - (:issue:`126`, :issue:`93`, :issue:`96`) Revamp entire CSRF handling. This adds support for Single Page Applications and having CSRF protection for browser(session) authentication but ignored for token based authentication. Add extensive documentation about all the options. - (:issue:`156`) Token authentication is slow. Please see below for details on how to enable a new, fast implementation. - (:issue:`130`) Enable applications to provide their own :meth:`.render_json` method so that they can create unified API responses. - (:issue:`121`) Unauthorized callback not quite right. Split into 2 different callbacks - one for unauthorized and one for unauthenticated. Made default unauthenticated handler use Flask-Login's unauthenticated method to make everything uniform. Extensive documentation added. `.Security.unauthorized_callback` has been deprecated. - (:pr:`120`) Add complete User and Role model mixins that support all features. Modify tests and Quickstart documentation to show how to use these. Please see :ref:`responsetopic` for details. - Improve documentation for :meth:`.UserDatastore.create_user` to make clear that hashed password should be passed in. - Improve documentation for :class:`.UserDatastore` and :func:`.verify_and_update_password` to make clear that caller must commit changes to DB if using a session based datastore. - (:issue:`122`) Clarify when to use ``confirm_register_form`` rather than ``register_form``. - Fix bug in 2FA that didn't commit DB after using `verify_and_update_password`. - Fix bug(s) in UserDatastore where changes to user ``active`` flag weren't being added to DB. - (:issue:`127`) JSON response was failing due to LazyStrings in error response. - (:issue:`117`) Making a user inactive should stop all access immediately. - (:issue:`134`) Confirmation token can no longer be reused. Added *SECURITY_AUTO_LOGIN_AFTER_CONFIRM* option for applications that don't want the user to be automatically logged in after confirmation (defaults to True - existing behavior). - (:issue:`159`) The ``/register`` endpoint returned the Authentication Token even though confirmation was required. This was a huge security hole - it has been fixed. - (:issue:`160`) The 2FA totp_secret would be regenerated upon submission, making QRCode not work. (malware-watch) - (:issue:`166`) `default_render_json` uses ``flask.make_response`` and forces the Content-Type to JSON for generating the response (koekie) - (:issue:`166`) *SECURITY_MSG_UNAUTHENTICATED* added to the configuration. - (:pr:`168`) When using the @auth_required or @auth_token_required decorators, the token would be verified twice, and the DB would be queried twice for the user. Given how slow token verification is - this was a significant issue. That has been fixed. - (:issue:`84`) The :func:`.anonymous_user_required` was not JSON friendly - always performing a redirect. Now, if the request 'wants' a JSON response - it will receive a 400 with an error message defined by *SECURITY_MSG_ANONYMOUS_USER_REQUIRED*. - (:pr:`145`) Improve 2FA templates to that they can be localized. (taavie) - (:issue:`173`) *SECURITY_UNAUTHORIZED_VIEW* didn't accept a url (just an endpoint). All other view configurations did. That has been fixed. Possible compatibility issues +++++++++++++++++++++++++++++ - (:pr:`164`) In prior releases, the Authentication Token was returned as part of the JSON response to each successful call to `/login`, `/change`, or `/reset/{token}` API call. This is not a great idea since for browser-based UIs that used JSON request/response, and used session based authentication - they would be sent this token - even though it was likely ignored. Since these tokens by default have no expiration time this exposed a needless security hole. The new default behavior is to ONLY return the Authentication Token from those APIs if the query param ``include_auth_token`` is added to the request. Prior behavior can be restored by setting the *SECURITY_BACKWARDS_COMPAT_AUTH_TOKEN* configuration variable. - (:pr:`120`) :class:`.RoleMixin` now has a method :meth:`.get_permissions` which is called as part each request to add Permissions to the authenticated user. It checks if the RoleModel has a property ``permissions`` and assumes it is a comma separated string of permissions. If your model already has such a property this will likely fail. You need to override :meth:`.get_permissions` and simply return an emtpy set. - (:issue:`121`) Changes the default (failure) behavior for views protected with @auth_required, @token_auth_required, or @http_auth_required. Before, a 401 was returned with some stock html. Now, Flask-Login.unauthorized() is called (the same as @login_required does) - which by default redirects to a login page/view. If you had provided your own `.Security.unauthorized_callback` there are no changes - that will still be called first. The old default behavior can be restored by setting *SECURITY_BACKWARDS_COMPAT_UNAUTHN* to True. Please see :ref:`responsetopic` for details. - (:issue:`127`) Fix for LazyStrings in json error response. The fix for this has Flask-Security registering its own JsonEncoder on its blueprint. If you registered your own JsonEncoder for your app - it will no longer be called when serializing responses to Flask-Security endpoints. You can register your JsonEncoder on Flask-Security's blueprint by sending it as `json_encoder_cls` as part of initialization. Be aware that your JsonEncoder needs to handle LazyStrings (see speaklater). - (:issue:`84`) Prior to this fix - anytime the decorator :func:`.anonymous_user_required` failed, it caused a redirect to the post_login_view. Now, if the caller wanted a JSON response, it will return a 400. - (:issue:`156`) Faster Authentication Token introduced the following non-backwards compatible behavior change: * Since the old Authentication Token algorithm used the (hashed) user's password, those tokens would be invalidated whenever the user changed their password. This is not likely to be what most users expect. Since the new Authentication Token algorithm doesn't refer to the user's password, changing the user's password won't invalidate outstanding Authentication Tokens. The method :meth:`.UserDatastore.set_uniquifier` can be used by an administrator to change a user's ``fs_uniquifier`` - but nothing the user themselves can do to invalidate their Authentication Tokens. Setting the *SECURITY_BACKWARDS_COMPAT_AUTH_TOKEN_INVALIDATE* configuration variable will cause the user's ``fs_uniquifier`` to be changed when they change their password, thus restoring prior behavior. New fast authentication token implementation ++++++++++++++++++++++++++++++++++++++++++++ Current auth tokens are slow because they use the user's password (hashed) as a uniquifier (the user id isn't really enough since it might be reused). This requires checking the (hashed) password against what is in the token on EVERY request - however hashing is (on purpose) slow. So this can add almost a whole second to every request. To solve this, a new attribute in the User model was added - ``fs_uniquifier``. If this is present in your User model, then it will be used instead of the password for ensuring the token corresponds to the correct user. This is very fast. If that attribute is NOT present - then the behavior falls back to the existing (slow) method. DB Migration ~~~~~~~~~~~~ To use the new UserModel mixins or to add the column ``user.fs_uniquifier`` to speed up token authentication, a schema AND data migration needs to happen. If you are using Alembic the schema migration is easy - but you need to add ``fs_uniquifier`` values to all your existing data. You can add code like this to your migrations::update method:: # be sure to MODIFY this line to make nullable=True: op.add_column('user', sa.Column('fs_uniquifier', sa.String(length=64), nullable=True)) # update existing rows with unique fs_uniquifier import uuid user_table = sa.Table('user', sa.MetaData(), sa.Column('id', sa.Integer, primary_key=True), sa.Column('fs_uniquifier', sa.String)) conn = op.get_bind() for row in conn.execute(sa.select([user_table.c.id])): conn.execute(user_table.update().values(fs_uniquifier=uuid.uuid4().hex).where(user_table.c.id == row['id'])) # finally - set nullable to false op.alter_column('user', 'fs_uniquifier', nullable=False) # for MySQL the previous line has to be replaced with... # op.alter_column('user', 'fs_uniquifier', existing_type=sa.String(length=64), nullable=False) Version 3.2.0 ------------- Released June 26th 2019 - (:pr:`80`) Support caching of authentication token (eregnier `opr #839 <https://github.com/mattupstate/flask-security/pull/839>`_). This adds a new configuration variable *SECURITY_USE_VERIFY_PASSWORD_CACHE* which enables a cache (with configurable TTL) for authentication tokens. This is a big performance boost for those accessing Flask-Security via token as opposed to session. - (:pr:`81`) Support for JSON/Single-Page-Application. This completes support for non-form based access to Flask-Security. See PR for details. (jwag956) - (:pr:`79` Add POST logout to enhance JSON usage (jwag956). - (:pr:`73`) Fix get_user for various DBs (jwag956). This is a more complete fix than in opr #633. - (:pr:`78`, :pr:`103`) Add formal openapi API spec (jwag956). - (:pr:`86`, :pr:`94`, :pr:`98`, :pr:`101`, :pr:`104`) Add Two-factor authentication (opr #842) (baurt, jwag956). - (:issue:`108`) Fix form field label translations (jwag956) - (:issue:`115`) Fix form error message translations (upstream #801) (jwag956) - (:issue:`87`) Convert entire repo to Black (baurt) Version 3.1.0 ------------- Released never - (:pr:`53`) Use Security.render_template in mails too (noirbizarre `opr #487 <https://github.com/mattupstate/flask-security/pull/487>`_) - (:pr:`56`) Optimize DB accesses by using an SQL JOIN when retrieving a user. (nfvs `opr #679 <https://github.com/mattupstate/flask-security/pull/679>`_) - (:pr:`57`) Add base template to security templates (grihabor `opr #697 <https://github.com/mattupstate/flask-security/pull/697>`_) - (:pr:`73`) datastore: get user by numeric identity attribute (jirikuncar `opr #633 <https://github.com/mattupstate/flask-security/pull/633>`_) - (:pr:`58`) bugfix: support application factory pattern (briancappello `opr #703 <https://github.com/mattupstate/flask-security/pull/703>`_) - (:pr:`60`) Make SECURITY_PASSWORD_SINGLE_HASH a list of scheme ignoring double hash (noirbizarre `opr #714 <https://github.com/mattupstate/flask-security/pull/714>`_) - (:pr:`61`) Allow custom login_manager to be passed in to Flask-Security (jaza `opr #717 <https://github.com/mattupstate/flask-security/pull/717>`_) - (:pr:`62`) Docs for OAauth2-based custom login manager (jaza `opr #727 <https://github.com/mattupstate/flask-security/pull/727>`_) - (:pr:`63`) core: make the User model check the password (mklassen `opr #779 <https://github.com/mattupstate/flask-security/pull/779>`_) - (:pr:`64`) Customizable send_mail (abulte `opr #730 <https://github.com/mattupstate/flask-security/pull/730>`_) - (:pr:`68`) core: fix default for UNAUTHORIZED_VIEW (jirijunkar `opr #726 <https://github.com/mattupstate/flask-security/pull/726>`_) These should all be backwards compatible. Possible compatibility issues: - #487 - prior to this, render_template() was overridable for views, but not emails. If anyone actually relied on this behavior, this has changed. - #703 - get factory pattern working again. There was a very complex dance between Security() instantiation and init_app regarding kwargs. This has been rationalized (hopefully). - #679 - SqlAlchemy SQL improvement. It is possible you will get the following error:: Got exception during processing: <class 'sqlalchemy.exc.InvalidRequestError'> - 'User.roles' does not support object population - eager loading cannot be applied. This is likely solvable by removing ``lazy='dynamic'`` from your Role definition. Performance improvements: - #679 - for sqlalchemy, for each request, there would be 2 DB accesses - now there is one. Testing: For datastores operations, Sqlalchemy, peewee, pony were all tested against sqlite, postgres, and mysql real databases. Version 3.0.2 ------------- Released April 30th 2019 - (opr #439) HTTP Auth respects SECURITY_USER_IDENTITY_ATTRIBUTES (pnpnpn) - (opr #660) csrf_enabled` deprecation fix (abulte) - (opr #671) Fix referrer loop in _get_unauthorized_view(). (nfvs) - (opr #675) Fix AttributeError in _request_loader (sbagan) - (opr #676) Fix timing attack on login form (cript0nauta) - (opr #683) Close db connection after running tests (reambus) - (opr #691) docs: add password salt to SQLAlchemy app example (KshitijKarthick) - (opr #692) utils: fix incorrect email sender type (switowski) - (opr #696) Fixed broken Click link (williamhatcher) - (opr #722) Fix password recovery confirmation on deleted user (kesara) - (opr #747) Update login_user.html (rickwest) - (opr #748) i18n: configurable the dirname domain (escudero) - (opr #835) adds relevant user to reset password form for validation purposes (fuhrysteve) These are bug fixes and a couple very small additions. No change in behavior and no new functionality. 'opr#' is the original pull request from https://github.com/mattupstate/flask-security Version 3.0.1 -------------- Released April 28th 2019 - Support 3.7 as part of CI - Rebrand to this forked repo - (#15) Build docs and translations as part of CI - (#17) Move to msgcheck from pytest-translations - (opr #669) Fix for Read the Docs (jirikuncar) - (opr #710) Spanish translation (maukoquiroga) - (opr #712) i18n: improvements of German translations (eseifert) - (opr #713) i18n: add Portuguese (Brazilian) translation (dinorox) - (opr #719) docs: fix anchor links and typos (kesara) - (opr #751) i18n: fix missing space (abulte) - (opr #762) docs: fixed proxy import (lsmith) - (opr #767) Update customizing.rst (allanice001) - (opr #776) i18n: add Portuguese (Portugal) translation (micael-grilo) - (opr #791) Fix documentation for mattupstate#781 (fmerges) - (opr #796) Chinese translations (Steinkuo) - (opr #808) Clarify that a commit is needed after login_user (christophertull) - (opr #823) Add Turkish translation (Admicos) - (opr #831) Catalan translation (miceno) These are all documentation and i18n changes - NO code changes. All except the last 3 were accepted and reviewed by the original Flask-Security team. Thanks as always to all the contributors. Version 3.0.0 ------------- Released May 29th 2017 - Fixed a bug when user clicking confirmation link after confirmation and expiration causes confirmation email to resend. (see #556) - Added support for I18N. - Added options `SECURITY_EMAIL_PLAINTEXT` and `SECURITY_EMAIL_HTML` for sending respectively plaintext and HTML version of email. - Fixed validation when missing login information. - Fixed condition for token extraction from JSON body. - Better support for universal bdist wheel. - Added port of CLI using Click configurable using options `SECURITY_CLI_USERS_NAME` and `SECURITY_CLI_ROLES_NAME`. - Added new configuration option `SECURITY_DATETIME_FACTORY` which can be used to force default timezone for newly created datetimes. (see mattupstate/flask-security#466) - Better IP tracking if using Flask 0.12. - Renamed deprecated Flask-WFT base form class. - Added tests for custom forms configured using app config. - Added validation and tests for next argument in logout endpoint. (see #499) - Bumped minimal required versions of several packages. - Extended test matric on Travis CI for minimal and released package versions. - Added of .editorconfig and forced tests for code style. - Fixed a security bug when validating a confirmation token, also checks if the email that the token was created with matches the user's current email. - Replaced token loader with request loader. - Changed trackable behavior of `login_user` when IP can not be detected from a request from 'untrackable' to `None` value. - Use ProxyFix instead of inspecting X-Forwarded-For header. - Fix identical problem with app as with datastore. - Removed always-failing assertion. - Fixed failure of init_app to set self.datastore. - Changed to new style flask imports. - Added proper error code when returning JSON response. - Changed obsolete Required validator from WTForms to DataRequired. Bumped Flask-WTF to 0.13. - Fixed missing `SECURITY_SUBDOMAIN` in config docs. - Added cascade delete in PeeweeDatastore. - Added notes to docs about `SECURITY_USER_IDENTITY_ATTRIBUTES`. - Inspect value of `SECURITY_UNAUTHORIZED_VIEW`. - Send password reset instructions if an attempt has expired. - Added "Forgot password?" link to LoginForm description. - Upgraded passlib, and removed bcrypt version restriction. - Removed a duplicate line ('retype_password': 'Retype Password') in forms.py. - Various documentation improvement. Version 1.7.5 ------------- Released December 2nd 2015 - Added `SECURITY_TOKEN_MAX_AGE` configuration setting - Fixed calls to `SQLAlchemyUserDatastore.get_user(None)` (this now returns `False` instead of raising a `TypeError` - Fixed URL generation adding extra slashes in some cases (see GitHub #343) - Fixed handling of trackable IP addresses when the `X-Forwarded-For` header contains multiple values - Include WWW-Authenticate headers in `@auth_required` authentication checks - Fixed error when `check_token` function is used with a json list - Added support for custom `AnonymousUser` classes - Restricted `forgot_password` endpoint to anonymous users - Allowed unauthorized callback to be overridden - Fixed issue where passwords cannot be reset if currently set to `None` - Ensured that password reset tokens are invalidated after use - Updated `is_authenticated` and `is_active` functions to support Flask-Login changes - Various documentation improvements Version 1.7.4 ------------- Released October 13th 2014 - Fixed a bug related to changing existing passwords from plaintext to hashed - Fixed a bug in form validation that did not enforce case insensitivity - Fixed a bug with validating redirects Version 1.7.3 ------------- Released June 10th 2014 - Fixed a bug where redirection to `SECURITY_POST_LOGIN_VIEW` was not respected - Fixed string encoding in various places to be friendly to unicode - Now using `werkzeug.security.safe_str_cmp` to check tokens - Removed user information from JSON output on `/reset` responses - Added Python 3.4 support Version 1.7.2 ------------- Released May 6th 2014 - Updated IP tracking to check for `X-Forwarded-For` header - Fixed a bug regarding the re-hashing of passwords with a new algorithm - Fixed a bug regarding the `password_changed` signal. Version 1.7.1 ------------- Released January 14th 2014 - Fixed a bug where passwords would fail to verify when specifying a password hash algorithm Version 1.7.0 ------------- Released January 10th 2014 - Python 3.3 support! - Dependency updates - Fixed a bug when `SECURITY_LOGIN_WITHOUT_CONFIRMATION = True` did not allow users to log in - Added `SECURITY_SEND_PASSWORD_RESET_NOTICE_EMAIL` configuration option to optionally send password reset notice emails - Add documentation for `@security.send_mail_task` - Move to `request.get_json` as `request.json` is now deprecated in Flask - Fixed a bug when using AJAX to change a user's password - Added documentation for select functions in the `flask_security.utils` module - Fixed a bug in `flask_security.forms.NextFormMixin` - Added `CHANGE_PASSWORD_TEMPLATE` configuration option to optionally specify a different change password template - Added the ability to specify addtional fields on the user model to be used for identifying the user via the `USER_IDENTITY_ATTRIBUTES` configuration option - An error is now shown if a user tries to change their password and the password is the same as before. The message can be customed with the `SECURITY_MSG_PASSWORD_IS_SAME` configuration option - Fixed a bug in `MongoEngineUserDatastore` where user model would not be updated when using the `add_role_to_user` method - Added `SECURITY_SEND_PASSWORD_CHANGE_EMAIL` configuration option to optionally disable password change email from being sent - Fixed a bug in the `find_or_create_role` method of the PeeWee datastore - Removed pypy tests - Fixed some tests - Include CHANGES and LICENSE in MANIFEST.in - A bit of documentation cleanup - A bit of code cleanup including removal of unnecessary utcnow call and simplification of get_max_age method Version 1.6.9 ------------- Released August 20th 2013 - Fix bug in SQLAlchemy datastore's `get_user` function - Fix bug in PeeWee datastore's `remove_role_from_user` function - Fixed import error caused by new Flask-WTF release Version 1.6.8 ------------- Released August 1st 2013 - Fixed bug with case sensitivity of email address during login - Code cleanup regarding token_callback - Ignore validation errors in find_user function for MongoEngineUserDatastore Version 1.6.7 ------------- Released July 11th 2013 - Made password length form error message configurable - Fixed email confirmation bug that prevented logged in users from confirming their email Version 1.6.6 ------------- Released June 28th 2013 - Fixed dependency versions Version 1.6.5 ------------- Released June 20th 2013 - Fixed bug in `flask.ext.security.confirmable.generate_confirmation_link` Version 1.6.4 ------------- Released June 18th 2013 - Added `SECURITY_DEFAULT_REMEMBER_ME` configuration value to unify behavior between endpoints - Fixed Flask-Login dependency problem - Added optional `next` parameter to registration endpoint, similar to that of login Version 1.6.3 ------------- Released May 8th 2013 - Fixed bug in regards to imports with latest version of MongoEngine Version 1.6.2 ------------- Released April 4th 2013 - Fixed bug with http basic auth Version 1.6.1 ------------- Released April 3rd 2013 - Fixed bug with signals Version 1.6.0 ------------- Released March 13th 2013 - Added Flask-Pewee support - Password hashing is now more flexible and can be changed to a different type at will - Flask-Login messages are configurable - AJAX requests must now send a CSRF token for security reasons - Form messages are now configurable - Forms can now be extended with more fields - Added change password endpoint - Added the user to the request context when successfully authenticated via http basic and token auth - The Flask-Security blueprint subdomain is now configurable - Redirects to other domains are now not allowed during requests that may redirect - Template paths can be configured - The welcome/register email can now optionally be sent to the user - Passwords can now contain non-latin characters - Fixed a bug when confirming an account but the account has been deleted Version 1.5.4 ------------- Released January 6th 2013 - Fix bug in forms with `csrf_enabled` parameter not accounting attempts to login using JSON data Version 1.5.3 ------------- Released December 23rd 2012 - Change dependency requirement Version 1.5.2 ------------- Released December 11th 2012 - Fix a small bug in `flask_security.utils.login_user` method Version 1.5.1 ------------- Released November 26th 2012 - Fixed bug with `next` form variable - Added better documentation regarding Flask-Mail configuration - Added ability to configure email subjects Version 1.5.0 ------------- Released October 11th 2012 - Major release. Upgrading from previous versions will require a bit of work to accommodate API changes. See documentation for a list of new features and for help on how to upgrade. Version 1.2.3 ------------- Released June 12th 2012 - Fixed a bug in the RoleMixin eq/ne functions Version 1.2.2 ------------- Released April 27th 2012 - Fixed bug where `roles_required` and `roles_accepted` did not pass the next argument to the login view Version 1.2.1 ------------- Released March 28th 2012 - Added optional user model mixin parameter for datastores - Added CreateRoleCommand to available Flask-Script commands Version 1.2.0 ------------- Released March 12th 2012 - Added configuration option `SECURITY_FLASH_MESSAGES` which can be set to a boolean value to specify if Flask-Security should flash messages or not. Version 1.1.0 ------------- Initial release ������������������������������������������������������������������������pallets-eco-flask-security-72bf929/CONTRIBUTING.rst�������������������������������������������������0000664�0000000�0000000�00000013614�15257217435�0021714�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������.. _contributing: =========================== Contributing =========================== .. highlight:: console Contributions are welcome. If you would like add features or fix bugs, please review the information below. One source of history or ideas are the `bug reports`_. There you can find ideas for requested features, or the remains of rejected ideas. If you have a 'big idea' - please file an issue first so it can be discussed prior to you spending a lot of time developing. New features need to be generally useful - if your feature has limited applicability, consider making a small change that ENABLES your feature, rather than trying to get the entire feature into Flask-Security. .. _bug reports: https://github.com/pallets-eco/flask-security/issues Checklist --------- * All new code and bug fixes need unit tests * If you change/add to the external API be sure to update docs/openapi.yaml * Additions to configuration variables and/or messages must be documented * Make sure any new public API methods have good docstrings, are picked up by the api.rst document, and are exposed in __init__.py if appropriate. * Add appropriate info to CHANGES.rst Getting the code ---------------- The code is hosted on a GitHub repo at https://github.com/pallets-eco/flask-security. To get a working environment, follow these steps: #. (Optional, but recommended) Create a Python 3.12 (or greater) virtualenv to work in, and activate it. #. Fork the repo `Flask-Security <https://github.com/pallets-eco/flask-security>`_ (look for the "Fork" button). #. Clone your fork locally:: $ git clone https://github.com/<your-username>/flask-security #. Change directory to flask-security:: $ cd flask-security #. Install the requirements:: $ pip install pip install --group dev --group docs --group tests -U #. Install pre-commit hooks:: $ pre-commit install #. Create a branch for local development:: $ git checkout -b name-of-your-bugfix-or-feature #. Develop the Feature/Bug Fix and edit #. Write Tests for your code in:: tests/ #. When done, verify unit tests, syntax etc. all pass:: $ pip install --group dev --group docs --group tests $ pip install -e . $ sphinx-build docs docs/_build/html $ tox -e compile_catalog $ pytest tests $ pre-commit run --all-files #. Use tox:: $ tox # run everything CI does $ tox -e py311-low # make sure works with older dependencies $ tox -e style # run pre-commit/style checks #. When the tests are successful, commit your changes and push your branch to GitHub:: $ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature #. Submit a pull request through the GitHub website. #. Be sure that the CI tests and coverage checks pass. Updating the Swagger API document ---------------------------------- When making changes to the external API, you need to update the openapi.yaml formal specification. To do this - install the swagger editor locally:: $ npm -g install swagger-editor-dist http-server Then in a browser navigate to:: file:///usr/local/lib/node_modules/swagger-editor-dist/index.html# Edit - it is a WYSIWYG editor and will show you errors. Once you save (as yaml) you need to look at what it will render as:: $ sphinx-build docs docs/_build/html $ http-server -p 8081 Then in your browser navigate to:: http://localhost:8081/docs/_build/html/index.html or http://localhost:8081/docs/_build/html/_static/openapi_view.html Please note that changing ``openapi.yaml`` won't re-trigger a docs build - so you might have to manually delete ``docs/_build``. Updating Translations --------------------- If you change any translatable strings (such as new messages, modified forms, etc.) you need to re-generate the translations:: $ tox -e extract_messages $ tox -e update_catalog $ tox -e compile_catalog Testing ------- Unit tests are critical since Flask-Security is a piece of middleware. They also help other contributors understand any subtleties in the code and edge conditions that need to be handled. Pre-Release Checklist --------------------- To prepare for a release: - Update translations as outlined above - Update pre-commit and actions: ``tox run -m update`` - Bump version in __init__.py and tox.ini - Ensure CHANGES.rst has all the changes and acknowledgments Release Process ---------------- - Update CHANGES with release date - commit and push and create PR - make sure succeeds - a SINGLE COMMIT only - Create release tag: ``git tag -a -m’Release 5.5.2’ 5.5.2`` - ``git push origin 5.5.2`` - Ensure publish action succeeds (both Flask-Security and Flask-Security-Too) - update draft release on github - approve/publish(both) - verify works - THIS WILL PUBLISH TO PYPI - commit PR - use merge-commit - Publish Release on github console (leave fs-too in draft) Datastore +++++++++ By default the unit tests use an in-memory sqlite DB to test datastores (except for MongoDatastore which uses mongomock). While this is sufficient for most changes, changes to the datastore layer require testing against a real DB (the CI tests test against postgres). It is easy to run the unit tests against a real DB instance. First of course install and start the DB locally then:: # For postgres pytest --realdburl postgresql://<user>@localhost/ # For mysql pytest --realdburl "mysql+pymysql://root:<password>@localhost/" # For mongodb pytest --realmongodburl "localhost" Views +++++ Much of Flask-Security is concerned with form-based views. These can be difficult to test especially translations etc. In the tests directory is a stand-alone Flask application ``view_scaffold.py`` that can be run and you can point your browser to it and walk through the various views. ��������������������������������������������������������������������������������������������������������������������pallets-eco-flask-security-72bf929/LICENSE.txt������������������������������������������������������0000664�0000000�0000000�00000002137�15257217435�0021074�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������MIT License Copyright (C) 2012-2021 by Matthew Wright Copyright (C) 2019-2026 by Chris Wagner Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������pallets-eco-flask-security-72bf929/README.rst�������������������������������������������������������0000664�0000000�0000000�00000010057�15257217435�0020740�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������Flask-Security =================== .. image:: https://github.com/pallets-eco/flask-security/actions/workflows/tests.yml/badge.svg?branch=main&event=push :target: https://github.com/pallets-eco/flask-security .. image:: https://codecov.io/gh/pallets-eco/flask-security/graph/badge.svg?token=ZYS0AST5M3 :target: https://codecov.io/gh/pallets-eco/flask-security :alt: Coverage! .. image:: https://img.shields.io/github/tag/pallets-eco/flask-security.svg :target: https://github.com/pallets-eco/flask-security/releases .. image:: https://img.shields.io/pypi/dm/flask-security.svg :target: https://pypi.python.org/pypi/flask-security :alt: Downloads .. image:: https://img.shields.io/pypi/dm/flask-security-too.svg :target: https://pypi.python.org/pypi/flask-security-too :alt: Downloads .. image:: https://img.shields.io/github/license/pallets-eco/flask-security.svg :target: https://github.com/pallets-eco/flask-security/blob/main/LICENSE :alt: License .. image:: https://readthedocs.org/projects/flask-security/badge/?version=latest :target: https://flask-security.readthedocs.io/en/latest/?badge=latest :alt: Documentation Status .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/python/black .. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white :target: https://github.com/pre-commit/pre-commit :alt: pre-commit Quickly add security features to your Flask application. Notes on this repo ------------------ As of 7/30/2024, the independent fork Flask-Security-Too replaced the archived Flask-Security repo (now called Flask-Security-3.0). This repo is published at PyPI at both Flask-Security and Flask-Security-Too. Please consider changing your requirements file to point to flask-security. Flask-Security-Too was a fork from the 3.0.0 version of the `Original <https://github.com/mattupstate/flask-security>`_ Pallets Community Ecosystem ---------------------------- This project is part of the Pallets Community Ecosystem. Pallets is the open source organization that maintains Flask; Pallets-Eco enables community maintenance of related projects. If you are interested in helping maintain this project, please reach out on `the Pallets Discord server <https://discord.gg/pallets>`. Goals ----- * Use `OWASP <https://github.com/OWASP/ASVS>`_ to guide best practice and default configurations. * Be more opinionated and 'batteries' included by reducing reliance on abandoned projects and bundling in support for common use cases. * Follow the `Pallets <https://github.com/pallets>`_ lead on supported versions, documentation standards and any other guidelines for extensions that they come up with. * Continue to add newer authentication/authorization standards: * Support for Refresh Tokens (5.9) * Support username recovery and changing username (5.6) * Support for changing email (5.5) * 'Social Login (OAuth)' integrated (using authlib) (5.1) * WebAuthn/Passkey support (5.0) * Two-Factor recovery codes (5.0) * First-class support for username as identity (4.1) * Support for freshness decorator to ensure sensitive operations have new authentication (4.0) * Support for email normalization and validation (4.0) * Unified signin (username, phone, passwordless) feature (3.4) * Two/Multi-Factor authentication supporting SMS, email, authenticator apps (4.0) Contributing ------------ Issues and pull requests are welcome. Other maintainers are also welcome. Please consult these `contributing`_ guidelines. .. _contributing: https://github.com/pallets-eco/flask-security/blob/main/CONTRIBUTING.rst Installing ---------- Install and update using `pip <https://pip.pypa.io/en/stable/quickstart/>`_: :: pip install -U Flask-Security Resources --------- - `Documentation <https://flask-security.readthedocs.io/>`_ - `Releases <https://pypi.org/project/Flask-Security/>`_ - `Issue Tracker <https://github.com/pallets-eco/flask-security/issues>`_ - `Code <https://github.com/pallets-eco/flask-security/>`_ ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������pallets-eco-flask-security-72bf929/babel.ini��������������������������������������������������������0000664�0000000�0000000�00000000326�15257217435�0021015�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Extraction from Python source files [python: **.py] encoding = utf-8 # Extraction from Jinja2 templates [jinja2: **/templates/**.html] encoding = utf-8 extensions = [jinja2: **/templates/**.txt] extensions = ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������pallets-eco-flask-security-72bf929/codecov.yml������������������������������������������������������0000664�0000000�0000000�00000000416�15257217435�0021414�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������coverage: status: project: default: # basic target: auto threshold: 0% base: auto # advanced settings if_ci_failed: error #success, failure, error, ignore informational: false only_pulls: false ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������pallets-eco-flask-security-72bf929/docs/������������������������������������������������������������0000775�0000000�0000000�00000000000�15257217435�0020176�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������pallets-eco-flask-security-72bf929/docs/.gitignore��������������������������������������������������0000664�0000000�0000000�00000000007�15257217435�0022163�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������_build �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������pallets-eco-flask-security-72bf929/docs/Makefile����������������������������������������������������0000664�0000000�0000000�00000012734�15257217435�0021645�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make <target>' where <target> is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Flask-Security.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Flask-Security.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Flask-Security" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Flask-Security" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." ������������������������������������pallets-eco-flask-security-72bf929/docs/_static/����������������������������������������������������0000775�0000000�0000000�00000000000�15257217435�0021624�5����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������pallets-eco-flask-security-72bf929/docs/_static/logo-owl-105.png������������������������������������0000664�0000000�0000000�00000047657�15257217435�0024417�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������‰PNG  ��� IHDR���i���—���uS�� �IDATxœíÝ ´­UYðñ÷pñ†¢€Wð’æ]ü–©y·HE -RIMÐP)Ô Nh 8È[Šha¥y-ïVj­,»©xÉ-EDÔ°à|ã7Ïúï±Øgí}Ö>à÷}cä;Æ{¯õ¾ïœÏýyæœÏ|榭[·ný¯ÿú¯é‚ .˜6mÚ4í½÷ÞÓ?þã?N×»Þõ¦ßøÆÓÖ­[§K/½tÚ¼yóøÿò^µ¡¯ÿí Ç7¿ùÍéš×¼æ´Ë.» Zû;ÍÑjÿ\éJWš¾ýío�ÏW¾ò•éãÿøôïÿþïÓ%—\2}úÓŸ^š”:ýñ—»Ðéþç¦ý×�ÞùÎwÆ|ç;ß™¾ûÝïNßúÖ·¦ÿþïÿ^¡å`-¹éMo:n~îsŸ›®{ÝëNW¾ò•§Ï|æ3ãáeµè{ßûÞx~½ë3#ÃÝ�Î9眡?üÃ?<}ík_Ö ­wÛm·Á´MŠ£4çŸÿùŸ§›ßüæÓ�ît§éóŸÿüôŸÿùŸÓµ¯}íí:ûÁµs b™˜6«uÆg _tÑEÓ7¾ñ�éªW½ê0wW¹ÊU³ºvÉçP³û·›Ž8âˆá“0 Gwß}÷í:\ëò<ÓùƒkñÅR±P®øÃÓK_úÒÁŠ€˜uñÅ¥™·\ÃÜ!îïxÇ騣Ž?þîïþîxáSŸúÔhxÙKÃWDpñý¼þ_˜Z}ÒLKÞö¶·M§œrÊP„ƒ:hÄ�Ÿüä'‡‰+x˜§ã¦K/½tkªH›þáþa83 :ýôÓ§W¿úÕÓ�þè�þOüe¯ÿÛÑ¥~"èO|âÓÛßþö鳟ýìtþùçO�|ä#§/~ñ‹ÓíoûÁ,qMóμ¹ÛušîÇ«_ýêãšÅ‰ýíßþíôÞ÷¾w0)¤V3k­ß—~™÷v¶ý�À±‘¶7OϲJó73ýÉŸüÉtî¹çó†)†{Üã#ü¾Îu®3èßuMºä’K¶Öؼt=ëYÏvóf7»ÙôЇ>tú±û±ÑÐê‹öQã]wÝuidëG„ã½µ.ϱÑ"Ÿ}÷Ýw!‹®y< Õüo``bÁ?Ï�ù6ù ﬧ�Ý3¤yó›ß<}á _Ì  pº÷½ï=ơ׺ֵ.C×E¸í¢ÃEÌ^íjWÀÿÝßýÝPU!¹N1FGÔØ_6tÙk^º¼¿¢ýþGôG#´wéç /¼L(»è=‘S0­~f5ò‹¤ øÍ·Á·h?š7Ÿóï2i[¶l™Þ÷¾÷M7ºÑ�¦ú§¿pÀÓMnr“i¿ýö[aÐzþ|—EјΩãmo{ÛÔž{î9bz!ºq”ð ˆGEµ±V‹ÿ/ÿò/CíIÖeÔzIȈ4ou«[M?ôC?4î Wç…‚�_}i�p uÁ™ ¬ÅTB¸Z+Ç|èCZ郟~Ï{Þ3pÝc�=Æ3þŸ7�çgþò/ÿr:öØc¬÷½ï}\´³˜;*ºíˆv m�—iŒë–·¼åô÷ÿ÷XÀýùŸÿùögvú‰Ÿø‰íÞíZ$™Fªnq‹[ '9-�tÑäË_þòéƒüà`ÒÝîv·Ñ·p•°œwÞyÃGÞýîwã >tZe’>ò‘� ¡BœÓN;mØ~æeu«Í^û¥/}i G<ðÀ!H, ‡ÿË¿üËÓ5®q�•w™.Œ!úÃfíÝï~÷À.w¸ÃÆ3úg~ægÆ»µºÿõ®ÍÇwÜ–Õ÷IÆ mºÁ n0Ì“©F@{Ògžyæ�ägV«¿‘5¢ßë^÷šî|ç;�D „‡ä_ýÕ_Mñ1=ÿùÏŸ>úÑ�N/xÁ ¦ûÜç>ÃÄú_Ô qæ!!¨y!pÑö7¾ñ�ã9áî]ïz×�içú׿þ°Õï0CL�K”KÓáŒAý×=†$¿ök¿¶ò̴ʯzdzà†_Ã]`Œ(Ù{ðÅÔ½öÚk0®ñÒ2×ÁW?�’ôû¿ÿûÓ׿þõA fM,O•!Ž_ýêW§ÿø�ÿ€òSw¹Ë]q�LSø€¹÷–·¼e´ýÄ'>qen�Ió.c€¾ö°‡�ÐhÊë_ÿú�4âBøÀ;8}îóŸc:ï꘠Mï0'$ºþ“™+c¸0‰`3Ì`FŸô¤' ¸Eaî#²P™Ùzö³Ÿ=´²aJQÜóÍæß˜DfŒÅ!3·à¦‘hG£0›�l$¢\hî\ óÔ8 ü† L@8A¶›IÆ�fƒc’$”§E$Èo¯zÕ«†Fº'bÃXæŒVéS_ïxÇ;†4k‹¦a¼¶ïÿû�ˆ,üÇ<ˆ{ÃÞp �i$³¦¿g>󙣭ßù�ßÑá­o}ë! L)6õÅt"F‹`™oÆ L×a‡64îúðùÓ?ýÓ/ÑŒp ˜¡!|´S›Úö.�ð.môZ¨IiÓ»Þõ®a:h‚@€JC†)�HÌ$U¤�˜=Ò�(&k™Ç$][Þ,éô‰Õ6©Æ8æÖû$c"MÃ\°ýôOÿôè/ÇNÊà %lÙa0É{ü†)/#Ýʤó%4$ÍÆP çõADtü°6à¢_ÂâyBDÛ]ð`ÞÐÁÿ\ŠŽÿøÇO|ðxn#š´Ð'eo!(’÷å/yÚgŸ}†:C}'á$υ𤉑pŒ„<IC|mñ ç3„¨LŠ>Ž<òÈ!ÝÌ'û¨G=j´��46&3aà8묳ÆwBÂü‚c•Ӧш¼·¹ÍmÑõÏG`þínw»q�%(RÃpÀÁó`DT&˜IÃTÿßóž÷QêÙgŸ=žû‘ù‘¡aè 8`…‚¶3Æ�Ì×d’†H'UåÔ̼5•î"­¦e˜ÐL ò<¤ö±�}l8_æŽt!¢ú�9Á}åœsˆ×�p�âÑH‘bð3¤1™NDÆ�ÖÁh,x¼K²}G(}‚Å;¦à†ÏÅ`¦ a1œU(8€7œX8f¾™y&šÖÚKàôM[à.æ˜)EKŒ < ìF¯Í[¶lÙ²zŒ�:²½ÔPð0$ˆ&L³�% tÎ6sÌ$˜9áH0„H§É�>Ä|x6G��Ú~ík_;ˆQïkŸf",¢Ó‚ÐÈòàÕfã—f0¢{ަ¸Çì|öŒÏ&í¿ÿþã9¿#8Í# "Vf5‡Ï·¾ƒ‘˜M»õím ¬àCã0]!*¦ýèñèG?zÐ æCøiÁdѵù˜cŽÙ‚à!×@P€E˜�õ;`„yB`!µ÷ÙdÏ!²ç˜,m2qþˆG<bH2Æüø�ÿøô€<`Hêûßÿþ¡ ü& ŽïˆKR™TJûA›gôÃÀÒ´øõà ‘Z¿ÑÒî‹.¼hH=&1Cüâ#¸ö0EÛ>h@Hi­ÿá…á &áÕ!ÁHïD�ÚgUø7´C&±‰Óµ׋®¡Ió£fj�‘hŽ{Ì#�@™@# D1 ƒø®4 ò¤ `Æ�AÌÄø¢ŸüÉŸ‹l;iãÓ\¢£æí´qÖ—ÏÚEЬiÍ4ó¡Ól&‚öz†Pù�‡s‰I´Ù_ójÚ¡õ~Aá_àGÄ%�ˆxÂ%ÍòAð‹@»ÐÁ}¸Ò¢_üÅ_\wžr½kóñÇ¿%¦„ Élô * xÒQÌ‚8'è9D"5l;Íq¢fx�‰šþð b#6)£)!8�æCJúÃ\Ž8S¦-fų$·¹µí�¿Å3BvħwôGøÜ3 € "WŒh9Ap�ˆ-¥¡ð/²¤­~Ã@VçMozÓ€¾ÌÚýüÏÿüèwg—G¶ HiL£ ÍO顨1IçÐi“è/„W'fÏÓÑbi‡“E(ÄdB2L–¿öÁ ŒÀh¿c"CÁhœ™Ïê_àBŠý�Až%á$ši"Õžõ>fÐ SOf4üVDošza �ð®ûžƒ¡ …)„�6ñ¯Ly4§(v£K"ë2iþÒp ó?` �¨Â[¦IÇI¿A’EƒË„ )dÒ=ôÐñ.mf#ŒÅH&›�!‚È#àCò�aZá9lÒÍÜ "¦   ð¡ ÙÚÄinpî/gn�ÞÔ�ߘ6‚D�ô�¸žGl00÷øÀF`!d´M[�}ìc®„e¿ï7í{£}G;޷Ħwg¯u™sHBû )Á�@#:˜)€0d™Rí9Γ0Ñ1i@Ñ™OΘ$"�}ÃÞ0ú#�œ:dùZBÂI:æ�b„3ÐôÁH~’)¢™ÚgŠù&ì1�yÌ4ã1x½õ­oøB~ˆ@Xæö·¹LÂD°´Éô�Ÿ0yžYwï�G2p gÔ`¾<‹–ë2©8¿d�˜Ùó1EÃŒ�db0¢¾1…¥$ÓH/¦¥~GhRŠ 6FB:)÷f19ˆ�GŒ³DO†Ì”ïf)ø3žg^�ÉH>ÆÒ÷ý�°ž�«)#8ˆF™i pù¾´ªÉQðƒ|±¾áÍ’dZ$hb†/ïRýºLšŸyà´›¸0‡&A„„‘|�B’Ô" !�1R�!îArÍy ã ,›b˜Î’2„»$ié6ÛLxh$?Õ: '­Á íg’™P¦“d„óa! L£�…éß)˜Ð_³,´UP�ø,�õA#LkÖ>„ɬ�míéò\kÎÝíèªcsy&M‰ >!¥$ b$›t2 Ë\¹ Ù`ØúN8aŒ�ÌÀóS¤8ÓI@h“ÅD1UK™ý1i`@ þÀóÌ©÷1Ã,·Á¶¶Db�÷Ì6I íçÏ™>|o^‘0åã<KXø!Œɿò+¿²Ýbâ÷E“Ö»Z"6‹8 Iq&†Füê¯þêð[5ͦ’øˆ¹o ‡ô1/|‰)ÔÍBˆÆ.üsf4Oâio3¢Mf�~ê§~jM$YL bzi"c¤çùT&Ð�€€‡æ"ZT '殇•`-àçyC�í;Áß÷ÕÜ­uÕ©éN!ˆÄAÂ"ód�G{HX³ˆ iÒŠ|Ba>sÁLÒ>ÎÝ|sÚàq~ K˜1b#8BéŒ ”þ›3£¥Ì§¶˜@ÚƒéˆN0Žù:Ä…“w[GjåÌ-&ú�`œôoÝŠù\ì(Ùf™k—ËórÓ1�Cü¦iø%Æô ޱUsdþò€gf\‚&„Ö�@m’nK„Àï4�ÉѾÔ(Ïœ|òÉ+!2"¶>¿…¸4”ÿñüË^ö²A@0Üï~÷Ï3¿½�¹îÁ…@a¢I]Äg¢á‹y-h—†÷Rp¡ÉDøPï(!f½kÚTg�÷afH?I”£‡9ˆkÂqýï `Ê8|á.Â0;ÚC@> ¢4†ï� 0�Ï¢]úñ;m5P¦­ž§m$ÜŒA«¸ˆ- ‰Éî�×"H´¡‘´�€€�xŸÙÕM„�{Úñœï„̶~'pÞÓgs€`n‰¦±ÞZ´¼Â™äB(¶aI{Nj‹fD��4ûKB‹pŒ5�Ë4°ûî‘RæA„I¦T‡ÙÂ(ï»çY¦„”2[K{0«%v÷i)×®àÂôO3žü"³•¿ñ nïé[_L0xh¡™fMà¿÷¸Ç�ñ‡ɧņ, ‰‰Ú™Ïk؈ŸÚ�±lÌÄn#2Éf³ýR á,q…™ c•Ð[ c 0¬ý8ˆà}ß™3Lõ<ÄL²º'Zb&™)%Å̘ç‹ ´dŽ€˜ÚâxŒ±Do‚¿Ñ(„Y.$’ä›�wÑ8Ñ! \–Ãïp‡p#D� 0a„`6�¤ÉÚh–~£Û {´ì6 È‘8OÍÙyÿ7~ã7†#Æ@ÅDˆ±Ûg|öŒé¢o_44�ãÃ}¾ˆ”#¤w9ä�ÁRÌ´´$¡oÑ%b,š‰ߟýÙŸ ³„à4þç~î熄›•@l>…&^kÏ=G­¹E„¸�˜¥à_1ŽMK1q†¦Áh( 8¹G+ÑÉóàamv&ˆØÐ8 à$ðøã�QÆ`Ä!˹ò99úè£Ç,�ÀøÀŽg»,XDÉvÓ<Z‚ùîy’L†ø9RœãvÑ ÏiB#*­¥ LÌ+^ñŠ´XÁxð"œ�u¦sÀÆÜ"0€ ~4��˜®j¶Ÿ6Â_{¦–ãÞûì3]¶*À"h s2‰à÷û2sxi[š·aŸ„ €$4%ÑÌ‹™ΓŸüäá<ûð‡?|˜’è»`¡üDÀ\’‹p„i$“¯ât�…ܧ±|"3—ˆ…1˜YÔ8ͲcÍD›Að®‰\>ÃÀ•ÿÔ¦¸þÀ$�a®Ëvua`éÇ+`eÖüw¿3—àÕ¿m‡ƒû„‡f¡¦‚sõn‰µ®ÖÇšÜÞ“tB’I ¤H?D1‹)Š@Mœ _i�hŽ£E|g­-Dg˜ÝÃ3¾³óÍgeÚÊÎ!¥ž'Ñe!.m}ÃÏÛ¾ÃÜšùÀ(34E;Ìf#žˆO¿˜Ò~! Ïæ$ íBƒL/!°è¯ ‡énÜG´�.Ëø#¸Î¯Ü.ͤ^ å'�tÒoEwˆŠ�Ì é„,Äüà$O=õÔöòU‚Ηö�fH4è4�9H!6 xK„Ct¦o£Yœ7ÿæÄ%,|ŸÄìи2—˜+} ›²ñ[³ûà¢-­MyÎûΔ5#ÏDí— „1¥´ ƒa¿µÿühT~ÈŽ®à‹îbm‘“Ý /&1qŒ¨ ¡8j’mlò =h¼ÏïÉŒo±ŽDs¶ˆŽiEJ1Ói ÓèDpéËoúöÒDÁ¨¢0‹ðÞ4è#“ŠÉ4[߈뻠ƒóg®Y YNüIÛTµ‹¡…AèA£JàÔ.Ÿ'DÖaùê|éFBï"Ü¥g8d¦�A„颀Æ4ÑÉg0Á˜'*nFÚ{MŸøŸ¶h“Ô’2ï#­2݃pLˆiï‘P‰ðžÅ`D.¤.)ÁõÍ ´ï=}ù]0#¯\D†¹Ð ?ŸA¨Àï]â¿D—†|šöáÃÏ€ÁÅì6wIS¡6ù^ZI³µçiÁLâ„”�|£†Æ²š¤c¶SòK:”ßÙõRŸH ©¼ù-n>�ýåmöÞä#MC÷˜o ‚%à›øDXK´‹-ù²¬VÆ1�ͱaMã´å�ÓLÇTÅLc(3�P`¡Ì©þ}à@ðh âÓÖÒh ¡ñ|Zê>Xù?FÓ2ŒlÒ˜9äió"Ä|ÎØèjcÛ évo-¸JôRÇ�! ‚·[�´’>å³Ë¦]‘i”{ PÀ3•€ I�"õˆ�xú(YÄó!’B(Ò, aZdˆjlù(ÚNh…� ÙRIó<—Öa8ôQv-“¤]}2«˜Á„‹ »ýæ¹òàþ’vÐKÐcºêÏxƘ/¬âÌ"­Yd v¡øRšäavšö�0I&E¢ Ìœ�� ‡y¢=ÒVúfb�Ä è}Ć˜w‡ùá„ÉÀc$&Q{žÓ&âx×ï�·ÀèyÑ”1ŒgÀ†èÈÌ_fQC K@šËJ2 fBib[~0�æ�ŸFÑíÑî&ˆKöwÓ¼ƒNhÑäóê«P½ký»35/ᜯ€ ›Éï rö›¤5YIB[ ra.âäKÌ{ñ9}íó¾-`~ù ˆOºØvD �f6¼ƒQ-_xž Á �Ì¢¾ô�É‚ks&1‘L‚‚sùÿƒež Ú†al�<Ñ cX÷ôaá·RäV›¼evú-eîØM’€8M$"ÊîWÙ–âxv¿LX~Ëw€•ì¯6M|?„P¾#»ÍÜ ¿íªBÓ8ÕÒx9êûK¿ôKC ô£½r1À€øfŒÆ÷0}טؾ×öP‰< AÃt–ƒ@1±´‡v§ðÐ'ÿ >VÀ3�_3¼ð®¨˜Ÿ_Ý™k‡L‚x9Ø.'±ÏýÚ¹ãÿòÒHèÙf]æŒ-žfKФK¤U"#çî]Z©]Á,±ùú@�RÏrèÚ!þi"`‹�Ž8ú5¶Œœx≣>‚pÑLp?ýéOŸ^óš× ü0àYôVÊt úÈ,z<ò h7?V:™ûü�)2>—™Ä´�½–öI¥Òšú@”ò¨!bl@+š?óG ±r°ýyßÝoNÌ{´Ç{Lä…¯"&HëÇÌ7ÇÍv»�y`A\á1Â$é´X Âlé“ RO0h%sÅ_� ´¶TæäLnice%a< nl$ú$œÚõ>Ó &pÐÞÆE|Ü|ÒéF¯N°¶!™47â ÀV3�i�O‹�_Z/©Â”(I#"bŠßšÂ"·“‚�`ßá~aD‘–<JCDùÛö;"b&SÆþƒ‘6#"â"Á‚ƒÿõ�Ñ` 4ƒÁN€´ç>³†áL§¶Ýgºüe*i-&X‚¦˜`TÑ«¿Ï}îs‡é†x•"XæZjp"5 Yíâ+¹ÑÊkΟ)p�ª›ÃΚßÀÔÌFÙ¦%\’2„œrû0 AI´û$ŸÉ4a€Ë éýõŒ‰Tÿc†ZÍmk%8¬)•0IÀ¯åú¶Æ”ÎìžvJðǘfK|÷ŒTeŒj OÌk§h zL³°zg˜´T+$�Oò¨n>‰Ö°»¤„) ¹¤Ïÿ1’"»ö¦z/©ô<¦""âµ€çf�F`BKæ$Tû­ÏxÖl†6òk`" GP0ÌÕ† M„ í;Þi‘9†¯¿ü#8›B"„‚I¨àC‹ö¿Ã¦}÷Ûo´ŒsNúƒ†ßrÁVª.)¥Q>:F Ì 9ÌIä ™0¾�ÐÞAìü Ɇ|°A €p"*ŒÀ DÐ&¢¸¬ I0!ùÞ³“®T*ÌB8M3™[š£má7-Ч6I;×t–ñ¡!(";07‰‘ÁLÈÀE�<�q„gD¾{ï½²màÌO›51Ó‚~îíìµC&¢ÌU�±Á ¤ !Û'’Aˆ¤Â$™tÓD ÷]›¤ ‘}÷—©ÃðM»lš®qõk ¦V¡õ…ÁÚDì§=íi£mæÈ}0HªüRXZ'b2=�Á4EÓvSŒÀHÅð°`F í—¬BÃ[ž0µ¤CƒùºH;s­n@ˆê—àG‚+<ˆA€%±œ0Iä8ù($hUk=�c²ü’ÌrÕÚæIºï|Ð�3­Á˜Â�¬gÙ’Û¦bŒcÒ7!2®Ò^uc›•&\‚ Z׿7ðzóø šVêÜÊ ¬Ö´ö„iíH7áôe|È"L³ä}£Û¢©Ÿe¯u5© ð8v�* Z…Ò@ü”ñ †@»¤=GžgJ�61üÉFÄÖa´IƒÚû#´–XBÓDNî›ËcòŒUhß|b‹~—)Ô>�j¿+a (|¢ƒU;p¢¬ŸR�Z‚ƒ~Óa³,CÑ£Âú¢é„^ó[]—Y™]}m>öØc·ìˆË™€�§Aùª ©6g‘V�"leÌ—•1D¢äÒŽ)í Š#$_As„ûíÚkSpÚLKH<¸ h�ßÌt#æ—ó†P­'�«™Š6žˆJopƒ«��Ìá€#¸¦Yj|÷œ%´4ä •þ°4“^�²SšdÏìv¿Î]ÍÒbˆÉ •ÂL‡8ë™; ŒèLÚM”zÞ˜"í©-¤÷¿ÐšôÒÌTF­ H>Ë﫳ÌMrÒhL$åÌ�.sÆä‚‹YJ[ø©Jhƒ_aBÝ£!ÍÔÓ4¸z–ehc@s„à3ƒAÃK•nW#&ƒ‹à ?»iÓŠŸÛ)&-»ž¤ÀA¸‚㤔½f‡9q_Å“Héö¿âH#â‘Lm�îˆ(tE,Äöæú½ X&£0œÆ .š…&�‚Aͺ3½„¢@¢4,Ä”¨BC<i'MÃ,þni œÍë;“ŽqC(š3¶3�nËŒNë,K\!LÚ4+KÝV~6—ÚÓ&„¬&$ 6‚ ¤!bjH‰ó]¤¸j%ˆ¢=Œõõy][„¡ý²îy¿H,úÄÄç=ïyCHøÀR§´ÁôJ,fV?-ÜùÛ©ýJv�´, O}êS‡@V{�–7¨fAàŽ‰˜E0õÙÂÞê*”K1iŸÔr.$ÚiÀ<µoˆ4‘dŒª¯e H�®RtÕ=`*ª%çžg´[0"RÓŸƒ'„x´�`øNûzž†!0SÅÔb‹Þh0ai «¬T¿é£�˜éwïù±WÑû~3y%a‚‰Zö?œø4–¿ÕO´Yí›vĸÁ¤i‰ÜäJÖ`’H‡V¼øÅ/—9c¢,0�'ÊÃl�˜ÄI"’JÄÅt&È|Üžð„¡YLÿV9‚É4Ãñªe®Ê&WÝ ¡´ãÙö3a|u%d31�´’éÆPmÜ–†%· I»žóœçŒß´Ù„0S¬=0š°Õ¯þhXS]k-ðí¨:Ê0wkÝ\¤Mn»<âT±úÖ¥X!baiŽâí7µh'¿»úã~R›±¶$‚Á’@0±Í<Tè©Ê¿’W°‰¦‚‹ôa:†0Éåf`2ÂÓ”ê�5¼À�î7Ú€xðág ÎipïðAð¡×é¯�� �IDAT}p0gÇ_³ÀK;á£Ýi®ÈjÍÙ‘¯ZŠI›fõVÙôöñp°•¸a:ÆöøÙß@„#Eþ¶'µdÄJ�5[QQ@í1_U †$Íb½[™4ÄGRLSõã^g?`¢Ó ZK`h‡g|/'A˜ïYí ßE™ Xù79{KÁÝ�·Ü;“[ž„a„ä“’7§Y¶Uõ^7zí0�IE5¤Ø_N£ ÉÖÁ=K«0 `4…DÚL:‰ç˜kE IïÐNvŸ™°#ÃûÌF›üEÐrÒ˜(Á�çÛÍ€øL‘{†ð¤)l;'f™ˆ5ßç>ØÍnÀ‹@ (üŸù”_î½½góu1ªU[‚ÃÉœhPž^eO§¹R×Ó:fî2š´Ý¯ ®’>tÊ‘øŠA• œEÄó˜ ™;Ì ­b<‡àþ7i꾉HApó¿v˜„Ó˜JwÒ&šæ´S�6 ·½KõO£äðéá´…‰ü©ö,™û­.3©_‚VUdšàæb­ÂTÌ P˜'ˆáSOhC -uÎùL¢iþl¤%"¾¥™3ª±PÞ7dÿðÿp–©™=Ë¥C‘n&Ä(�m÷¿í(—‚Dùܦ]š± e�ÔW’‡±ž b!Û/ꪢFb3ŠX4”�4hf²õQ�£jªb ­ÃÔifž1„NBeÅ©ôM¨˜<s‹Ú­bÁÑ´fO*@Ré·ùk™êKçݹ�Ðê¦ÆÙr󅤩"°ˆQÌ ¹$Ù‹ =U‹Ed|øá‡ õ<MaJ�f &’?C�3I?ÆÑ&cyœ•… &m9VPŠßð�Æ,�Lû{i�©¸Ñ}I«“ˆ®Mdˆ�ü¬°Úý"8‚A£´_á§JSÏo [+Ò[t-íÅH'giR�pjÌì´1‹†XÚVF( !ÌjMŠÉÃfƒ[8ä{<kå´ä ȉ¦¼IZIB9d»#šÇ㯴gàHšµáo;âÛößä¯þh.­ŒÉú`ÂøB¦KZM£ ˆkH€ø˜BPÛ‘Ñæì„'A! ¸˜^ðT„·¨�䎮¥Vf\)N„k^Ì¢[Kêr줃ÉCt!4I®.iÅ�~ˆô’ZH’.bD€<CÚŸò”§Œw’¸¦Y:o �æ·zŠY4 s0Ï3L"-À�ŽR¨²dùèži5¹U\»ÕiSÙMgZ©m4RÅø]tÁ}òs­QéÛ©¿3×RoA„m&ýù f­[fŒ™¡öÌ �BtÏð)�oÍG[å Ž ¢>Í(”@YvPg:@º’e¬_ˆ·Ã¼4ßSylãƒíÎØhâ£õãÓ29aÑsŠ™²\1'mñ.Mæƒ  ôa@lž’Oªx¥I[u^´û|Ùk)sW�S’BÊhŒQ:mq!’{�$Ÿ=þ­ßú­•ÕDÒ €pHá�[ÛÀì4h¥�ÍPk·Òh´ºb·ò h)‚‚É3„‘¼[U“H Ã[Ø£m~ÇŒk£OÂÆRЦ „Þˆ-´6ñÊR€ Óh0¢CÁëžòÀKÂÙèµ®¹ËÑ‘2ÒÅYvt�¿Ë,@€ «ðK¢½‡)´ŠOƒŸ`¶ò´C;ü�³�É4Äÿ˜Á1ƒ­¶bF‰*´ ‰ÝŠî‚Kho §ÍÍ´Y�ƒ˜ZdXY�fÀÁjœ•¶·EEù+øVå’)ÃsxÕp ¡L[¢Áò#¸Ìß™í™4Ê®\éJÓu÷Ùg˜±öÈ2ÔY‡L�äÅrçH¦Áe%=9pÒ‡¦ƒJj1 ‹™˜†˜¶MÊ:Õ>¿†ØÕÂ�jB xÑ]“¯f �6Á�Ñઔ�@4ðµÕ!cb—?F Ò¯0\°#Ó• ûñ‡Þ3šfÃð6{NÛX™­³ãa;¢ÙŠ�.þ­›w“hÕuÎ)†´h�I6³SGR&JâC„Îü bya x’Ê)“ìêŸV”�!�Lfð)Ú¯V�~iZÉŠÓ\PÁ?0­4¤�êŠh4¼`¢’oð)Ó¶suÁTæ·ýJ-×W9#õæŽ ÒwË1˜…`°Ì>-HØ_æZS“ZC¢ž¤ËZMY;e‰Ò#`³Æ.‹ÎHYéÏ{ž/·ÁHhUŒ;¡…ÉÐvUG*íY޲L™E¿!Bñ ´ŠYÁÈ–H¶ßÜa3ÛÞø\î ýÒÜÊÒ AéÂå¢ósÚ`1‚6šy¨ ~ùÞv«{·Í í¼Â˜4ÍMY�Ê’'KÚI¨ªN!ëÿXÂ:†AªâO®Vª¹í�˜ÕLy)0Æ_ý{Þsœ6†röÍ®ƒƒÆ�£4æ’L˜[L¢-Úªd› 1 îÄdß1ºÅfê«Ó@ƒZ�&�žmÉ[ý¸*þ�fÚá"Ì7²4à�ŸZ&�XȤM«Î>b*˜+’ÆnwŒ\ÁÊAcj\ƒ�%™·�8ægøÈ6ÃíYí–AÜóá (h¨¾h6Í �`i! hNa|[5;F¨ª'´=³7ÍŠè&dëãp,B ^A _¤_fUß‚x¢�©.ÏU�’Ö <™z0sðK›É.×`–䉘„²lr»üD`:4^*�rì:"C ‰ª®D[‚ff„™#UÓìP\maé‹E^ö—ëW"ãü¸-amÚƒ($^€‚I„˳͂€SiM'(Ó^¦´Òp`ìH¸Lha3Í^¸×¦5m±*ú$ �§QBϲ×vL*ìæôE9-) PÛß=C*tô{¿÷{cÁ $®©R*4å'0jìg:÷Ü•5 AX„©¨n#uß!"ŠÓga­’ŸîCœ¡¡˜‚éUÇÇ<ð#nøœðøÛ ˆÖ¬>é0 }ƒ�PX|¤©&Á†3ƒÞ£6Ÿ ¶‘i­�}Eêቱ´® ×�fRŒfšú°u‘ÊBˆä�€˜-&™­™dóK+H”oVÚ“¨/†6® iä4›íæ›'^!@í–sÎ2[Í^k§£DfeédFi`gg�övŠ#bÁŠg; ƒÉDxþ†å `äÓ„åžc’=‡I,B •h.mòAèFÐ¤ë· Ñ—‹I\MU©sÄAd‘›À Ã !W�>RJs™óF»ÖÝGÀUWe‘B>…9éšF;¢Ó\°@ÑJ¨Ôÿä™JÉ`¨vh^•ü™LGØr!Œ¯ê¿°¹ºÞ&~ ¼WÌ?�‹äm販<Oki_]¹7ïꯜôe®5ŸjâQã¤À*É“nêky[ÔGËÏ@!T¤BOÒB"…ß„%)vüA¹sÍÑ!«ãÙh"ƒñ�>ÄóéÌZ—¿mÝ÷lþ)XÉkãÚ§ÿ×½îu+e« �¤1„ÁlƒMåäa2aÄ€ÞÁ Bb]mƒ·ß¾+34\úa l™g#É’ 5 "k«%UF ÎÀ«ªí�8"U�Éw‹~…ž€bã«ÑŠ8U±o �ËßkÂ4(rÒÖ4Kí… ˜ØýŽ>€x›�ÛL]8Íœpâg~éÌÑçû�Ýï(& ,SN ôU5cíWlƒkw=f·*ýÝïl«ìR²g…àÕLÚhÅý…šîä°Îp Æ –bšÎÇÑ9g�µRó�âBYSõÖÙ)c>Ü\gH`8im 2Jäxàʲ}ÇÀµ%tœpyõm�ôŠNÄ$ à£~…Å¥)ªó¿qþÊÔ‘�‰™s-Fa8¿Ê‚0ï¢\‚�OL7¡Õ<* ZÑFÐf*Zhôá›vf‚u;&5 lüC:®04ÂSeH„‡�ŸB3ÌGXDñ`;ñ8~Ï#v³mµÁèQ³û–·ÚE3¼£�80æv·½Ý€¹ÜÚVjrMÉ­Ýê)ø¼ò•¯æÌ,Ç4Û9¢m}ä¿#Ÿ%@jóuÉ,4솞<«’é-¢}òAÐŽ6ÂyZUxpG×BsG;H äuÀÖ hG¡%$�Ø™¬ $¹ø«–„°æ WˆV‹!´+ÄΜh“/2§G:‹²æM$�|íUÚ4;�&ßV±ÂÖ‰a+µÙ,tûtÛàVéR”Fc­Ø‚Õó�ëÇ¿€=¤ÀßY´¥¾Šˆ KâÔFûláÒ.È® g •Q2}uvŒMªïÆD¨œ�`¢'ŒkSÂb¢w¢˜±m)ò!ñ¤ÚzL{T+üGò®|‡ !"žþ¤ )$³h!XüÕwsŽóE§™ß Þk‡z‡gÕ¡k7†¾œSKk™ö¢<k`,�C¼’Õè$˜2±Œ–èè=Ö†–£QÕ´ä¬ÃvLjæ»rΡhôÏ| 0éoÚ¿¤o"!Ìw˜wc K²D†m–nó$;„ª|>ä_Êͦyú+RÓ&-DÜÊ´y‡pè__úè°aÌÑVæ¶æ`Æ—ü"Ï»Ì38H¾v˜~8'Œ$�1X-ùŸ‰,ƒˆ@T¼“7²”¾“¦Ùñ�í;êPÂl=•§6‘Ñ(æ¤Cª„ÚÕImÖWôURå…]8˜ëˆ* T ­â蘉€i�§¥�âÈù Ïð�eðКõhków˜ì™æð˜Å2`õki‚Y—¶1cµA�/† ‡¶DlçÜ+ÏcÕ¹Zª­Ì2“™Þ .s]&pÈ™û›†V¯ƒüOjT´Øf­1K:$-Þ‘²J7�ô®›Þl [)LæÂ{$Ÿ7ÅÒ®@LÑ_[c*ýÒé—ž#¥ù‚–§I/ó‚€î·í„�UŠ Jø`ÕnšjVÄ3’NaÒžÌUÉœñµÚ”\É,2_p=î¸ãVfX¨q!­Þ<h,áï, 3©Q°ŽD5MVê°TƒÚ]Þ!‹žCèΧmm•§âBYïb~G$ÐRÒ«’UÙ �tg#uN_uƒ0‰YkJ†T\¶¬#E´SˆN‹šÓ«¬'F4ÀæwªðÈÔ¹ïY~Ù2‹¶Ì¨Ôbʬ%>L`ÄÑZ�†BS\�ºÆÎ·À$x± ;¥I1ªã H=¦!<B#83(�f!°ñ�)Ù¢I餓¤1//zÑ‹ã˜!Ñ Sã’&œ‡€ç1#[ò¼™eÚ2žšeàpÎUYç h�*,ÏŸ¢Ö „­ÐM!Ú‡·.ŸÑÐAŸA3 ´Á�Ÿª€LKš§ÓnU—ÁïaÁ4î¤�Ëh·óIUåBp‚ÀÍ*°ýM J(™f%-…âC Q�O;'�Ó¼Wí’VZWçßB[" ¹åjg²Hìyçž;ž-”EÐŽïé Y iÖ»Y„ŽÜf& ae :›Ðó–UhQ‹|4ƒF*òÕ®ºrD�6µá/+`–u!4ðÂ˜Ò 0# �°5õµSL*4|á _¸R²³eêÌ4Ð"HÒ0Ú%…‹)ð;éj[=¢yS,a�ž¢�€¾öž×^)‡£mFüöæ¶ '"£ ÍõEÓïí¯ˆzI“ˆ.Dor¡h4ÍÉ÷vò%‚ÊŽi(O�Ùbj •¶îÌ]iiEŠ|¬ÅGBK�Ñųp+Ù •+¨@ðN1  ^…Ñþo@)(€8¦© Ÿ´bB~Àˆoðð��ØÌeÙ?ˆÀÖ_pá¶šàåÔ‘> Ìn”ßö›�Ïÿæ¶¥Ò~Zß•�Ù}6P D£�ÍÕU­Úw˜•ÿmÉ!Ëqoµ•©-3¨dyìÚ¯>ø:È䃛€z¿‘™FѰ~—¹¶‹î8вQ™4HY) 5MÃywj—çøL3þùHÆQ}Ìd���¶¢¿¶+BH›?›Ý¸Å‡ T4#™£ ¾uÁ &Él.�¤^s�=Vêèy—$7߇a¤�f4C]ùšÎ�mí‰Ð”§ŽI‡èðExù­Ó7ÁY2í?æ˜c̘Ӝ$+!WžÆ-{]F“ —™6ÄFØr¹…�…Í6YÑ0Ï‘,õ¶�òÒÖ€Aœÿ�Œï¢ÀÒÏ5­¢�N—$Y�#�q˜ŠXÍ ¶6«ô¥ïæÚ<�褼RÍÞaûiMfÖ¼ÏT15ÕnÅ øZ÷"Œü <; ¸„L^A„çÁY.ËÃ�7Q̧v¦†{Þ§eð§´rQáÜE×e˜TXÉí4ʪ¡t =Fø]Çœ&§ °L ÈMkU—$qĈH¢«[Dô�˜´�¡ýÞê)ûŽ�¤“¹ AU-©ÐaÌ3%]4¾ÅÅ`j£%lÏOJü¬´f’xÄDƒÌvU(Ñ€UÉÎ犃‘`™±ÐŽÙ ýs„�v£ Ao[5wt­˜»²w€EhÂG� Ñ1F‡ÕHÅ<_ΟÉhµʽ#±4©œp’ê}¡»ÙcRëy„"Éü†yñ«†B ªBÜ,7f2mEVÓlK "”ÃàýöÞ‹odò>þ‰�¯9´ÀWÎ83Gi8¸ªÀ¢/B„ñ„¦B´ÊxJ�ÔZ_Î,VpjÚ™¹»M³S”±de³™«& ä …“À!‹ä²õEvãb£]Ù)ü´§\nï‚€€ÍÀ)}äÃh:¦V±K»¾‚Ö’ “}GÀ—Ž¥ãÿÀrÖ—¶iBEç;R§rmpgÂÀËŸòÉ´¥¼A‚M 0ˆ jç·û·GHÞ0 ¥xtЇ¨˜�nhœÔÜS'eU‡0�rÐòTq&¾ Ñæ+/z¾q[ìÙvÔ5…ƒP˜nZ¥“5™€¶´ ,Á!˜ØpŒä¨…È%à›©†-�cïæ[ƒjö½à0Åå7Ó@|KÑ^Ü–ÀC _ò’— íÔw/ò= ‰åqÐN&”Fé«‚�èÀdÃgÉ(­:6}¢“l1“�YУ ¤Ñ3b¾«é#�xpüSÛT˜9H�~Áœ–9Üç'ÚÆR^…~9ëvi´ÃáüÖ¬�aÞæ+o!jµYK®Je¥CKšÄ,û_› aÂ:®ã+Nßn?šDkµMƒ}OsÁ“é£D~}Á“†T-›{w&å$îÒ­— �³&2™f‰“•eaØo3$„ºW"³ƒÜ-�Ó*„jG F�Zç8L³€"$SˆIš;6�‰å¿ô­MM3ªBN�QL™ûM idz´ Óù£ãÊ#\³Œt¿EFôaþŸ¥mv„wU̼ÓD.X†Ü©¡eÇ‚]Š3†.›-´p0;ê*\g¯•³aÛòÏ0n‘‹Ö!H‰&"Fó:˜¾PÀ¤‘É1÷XBÑÚLƒJÒ‰1¢ÁNãlÊŸ„Šª|//ˆI“ÀKÀÀ[Ž8øªºUq'¸J>‚P´•´ÌVc'}#z‰›îw48!KKíú.Mº3š˜æ–åÑ�P3é­Ì.“¾“ZnÖb`sE|VFPÅ�„¢íbw9ÿ6430å9Ìk)½IK‚p¿ûßoúÞÅÛ*0"Í€°¾ÛöØ ]þ¶u¥ÄâÚïÀ­’ð½clø‘f…É´·ƒA¹ó5´ÝYL<í›?ÝÑiŽ>à'úgÍV¼ÛÌ} žË ß©•Ù’óDG�F``º˜,>†¶CAÂÆ1 …¨ ’‡�f kÙ|ÄDDÚÅäÖÿ�·wG ÂŒ¶u¿T-‘BÓ#½úÇLÑñ<Í+2U•73—¨Of‰&ZÃ\¦»qßGÂ�`P©Ìˆ¬/ý‚YpCäÂsÌ~ÌZø��÷1�ÀÓm•Vn$¥ë2LÚ:;è·Ì–ÎWÁQiƒP’”[ÐB�$œ$‘�¶•XŠ !ˆŽÉˆäymrºvõ‘f@#ˆ�°{~ó<mEñ7sÂ|Wy^_�S2 MOŽO¦ˆ¦Ò|C ¦‘[g1ªêEk Lù/Œô>3ZÊs-x3`ƘTŒa-ø¡6¿ù>°³<6̤®j³•ÝbÔ]]8„®f¿;¹ßÿÞst5‰ Šò8Y&A0�1Ä8 #ù|�q¢WW6©>´GJI.¦ñM�iKsI+�2Þ`’·3fùï;ÁÙ 1¡ÓVS<$™Åè88C Y–ƒ M³JÈ�7X`@øø;Œ¿Ü:Øl7˜:<ž-L*5À áá°ÑÝ~Û1©© ~…ú2êÚ‘,>Øe’Ãd@¶Ê]�©~`! qÒ !&˜¤¢��‘h& “SoÎ=D3å¤�N0Y<’hJ ª¶¯„g>%<–ÈÈì1‰­âÒžê‹gpô?æ¦O4æŽO%H�”Ƽûñ˜À†•·wø#Ú·ÌaÀ;dÒ4Û¨[½U»'�rÔQG­ŒÚç�—&ù$›™ŸBX�ú £1ñ×ýׇÚ[1¦bµHWj—í ¡mÓ$}rÉ ISÿB` Ž)EŸ{\s�é€ý8øò)úBp&›ïd’ô f„&@í2$ù¬óĬƒÍrM¥±L#m� þ�¶Ö¸«½T¬9òµw…0)�þ£h¨“Ã�K:HV5R«’Ï1;D23¼/Ò;ù䓇ù+͘Ä">að—ï錉2h[®@ŒÖ®J€dÿù�hÌMÒß©aüüv�ƒ¯í) >·Ô3’Ïü hЀ Á·1jèA+™hZ(ÿpš•õq�§ /ÚàK™ÔT2Ÿ¹Öd+SЮ:È”'À¤˜ÓKÊ9Ó¦Bšö°¬Áñ#¤Š&UL–6v²>óCÒ0N´…@ŽAM¸z¿h¯H’ïáÿôeÀXþƒgLEé«CFüÞÖQæs+Œ_‰§,8áé;S¬ Lü£¾E˜Õ飉ÚbªËæ…ú Ü[u&Ìm�–<*nZ�I^nSXÑVùÏåW³ùÄ„sHµe Ã6À2K9bc4�™mW¡ü’g˜1L8”ìṤ¿Ã>Ð39ç=¾G�Ó l�ðÒ‰4¬-š[kN¿�@ò™>3"𳛑YC pmJ¬aJUòÂ@&F·œS¡,øœy/s­É$’Uñ‰¶T¾Ú=¥]AÔ=R ÚcnŽ4[Oé#Ä×fU�Ó÷?`ÿaª<d´Cú«–ej©“ÀÌ‹un“~1‚¿ Õ4J_Þ­ha×I3‚ñGVR=ÃG¸MàA�(©¿%mclÇ yŽ9wŸ�ËVb Áá ý•DZÞº~æ³L­»55©†rä�OIõ�g™3C›¬lb?BB1¦S»*ÜÄáV$pÌï·mÚ“µÓL·{Uðò¼¶hRÕXŠBùKìC“üf®±³Ì;ݬdKfÑš–ª.Q™²˜#Ķö#’$”`aâh/Øø#ŒÃ0XŽ=öØ¡9¿ù›¿9Ì~§SÏo.ð»1ÒjF-s- ²•DuSW�Ù�°sô<CšK†4'Î_8šwΑ{�iSsõÅ;vNÿ¤b˜XX�бo¥wžE+žÌJõêD~m᯴§1Z+Â` ˆOûh93ýÅ3¿8´¯³`k¥—‰VÅKhM;iwÌ´xÎL¬gš2Ú™rjÛ1©J(1‰„‘H�šÿšf›´t 8š…¸¢ö¸ÕÑJ@”sE‚U¼ ᙆá1ƒi"‰L{™5ZÂ×ar»¼i…û|Aé(lÎé–v¥{†yíx8‚ü2½4Ê aiîNº4b6¦ |�+8îünÚÖi8̰çM%ù[T;¿ååriR+´iU)Xü)N3"Dó,)•Sb{DdöH´ÐTÄ# GLŸ¢¶Ò|« 4VLÏ:kåx÷rà0šIj[MiÚYyýu8ßH’Tÿ˜Ä„�Bä¶Ñ"~¦ÊÄ4l¾Æ+¦‰<ËŒ¥Q’ÏšXäô?| ßI³Û¥¸‘Ò�;dR-f•ÒÕ Í”ˆÉM¾‰ ‘kçYS 4��?þø•âI¤2˜JJµ…àúñ3A#r„æÚÝ�Yß™±�.�ï2v°T"¦¤}›{¹aÔ4ÛwKc1È=„+“ �Ÿ¡o>�Fš›Ä$su˜ƒáï"Á²mYÿ4ðóù­}]Õ ¼B˜´ˆiiKÛ`h‹�›tŒüì}öYÙÍ IA0HVƒSÚë/‚s¤�eHs³Ê|†bSY àÿNÓ&æ»Çw1Iò³1Î=„)·qø±i¶¥1m˜Ðý#>;bUŸÚhy#K‡&tðèüvti)‚@U´£°¿9Ðï “ŠŒŒÄ%üA�Á�gM@�ÚcTËÆ€&�~'}•@£aîÑ< {±üQËêúãtù9&R3¤™P¾Dxß%ˆˆI`j×!&bF¹mÌW㡚‡9¾´ÕÜ ¥0Ò(Z&‚¬>æ³�­Wa&íé™ùç‹0­��T|dë´uÚ4-?ë°t]pˆsòT½º¢l=ÛËì‘VfÈÈÛ,�S€�l40{œ8³QRaÓH-Æ”¤BÓ|m1CÆPeŒXb¸öH,B{ÎoL¢•¿ç]šQZW•…=_'KÐÒ{›Íh*؇qŸe¼Ó2ƒ÷:_I�‡¶ìÐTý3s†™9°•A»lBäüµR”p¾fõvÍ꾑DÄ:餓ÆÈ»Ä’ÇQ“D’‡H¢�Nó'Õþª5„H´†ôó#$VTDêÊ4Õ. ø>NÛ †ß™Cˆ·«Á=ó½a„è,?í�‘ÀnºŠ6Ñ$̨j ³Ätrö~7ƒafÞ³˜F)p 1¥zi›Éó?Ô&5Ô†9¿aœÊ·[‹Ö‹®]z¡„е.©l‹ÓZ:Ÿ�Tò�Ë��^IDATiÃ ŠæbLgüaŠ96Ì ]˜‚iˆÔJÆ4gšõÃGaFIüd”�(}˜&„õ‡¢éúFز]Mçtà0íîÄÊο�W«µúbÒ1ˆöÓ¡¹!DÕ"g,I³í>´Ö÷Ì¿ÕWf²ƒ�KOk}jÙùºùk%ïNƒ��iBTT¶sd­Îr�ÔÞDbk>Û˜„†©3yÊ .0Â6�xÚfF}g‚˜Œ3ù%D©x³Š0½ƒ©�° ® Z­�ò„Ø`möžù¥}Ì"KÁÄiÌàP >¦Ñ@šÕáóîL¥x,¡4ßÇtK+hÇ�訽ùÈn­”E¿]æhžùñÑê óˆy®ìMR"Yߘ@¦vJBÄD·T-f‰­†tùÐÞƒp©ÂL )Ç ÄæÛhaeÝ0¼$B�‹y£ÙÊËüd!˜9Ìc^Á€I�Å×þYÌ2WÈDñ±ë…�èUð£>Ö3Å÷Ò6šÞLº ¢%ü.}´Ô3­s¨UŒ[ýûÊÑ<;ª²;ÏÈD˜¢TõãH5 îDd„t¿Á.„Û�M£ ÌUä#R[HúÛØÅ÷ª¯ø¿Â‚Œá"00De®,°¤ÕYExp Ê÷ž?ú›¥èpïfR §¾øc}Ñf¿O3�n“@‡Ï›¸EŒè;Ú`6:lÇ¤Õ ¬þtÈà4Ø6¯c+,[t†)$—TŠ’H%Bè« ´Ý²ÙfDi‡Be¨;a™ÿŠ FøÄ£q`©Ñ6þ HËžE¬´¡Ú*¶4gèÙJ²1�´«Ü Ñ?8ÁÖ!(\Òny÷Û~³lÉ´è-Šdaš#ôÛ®-ï6ËÜ4P'퓦üU[#IÞüT�y�Ùhå_;ì°9™}(0…–HÓGjÙueæ0—–`œþ1�FtPbuŒ N*J3À‰)"Á ˃«Âñ˜Hc¼Fßõ/¬Ç Ž$5&ƒ“¥›wÉrG³‚ÓðCßLkæ*Û–Ð66Z/P˜×¨rÏ«Z¶r¶Òé§Ÿ¾•f�Dbï;OâÜÙâŠN@¬3eý>/5‹®s� †µGúh)ƒ�­"¢©BÕ„ƒIäœÛïÔ–Áªýr¼…Ù%�t.{•#ù0„%�üekN´éÔSO]9’” c!ÐàÔßÖÊÚ››¿iœU9¸Î·­¤Á´ÀÿÌ �=EÄÕ¿“¹Tùî^›O9å”-¤²ãFEgÌé5–aÏÈÎ3­,¶+°ñFšî37-5�Tú`Œy-IL ³e¹¢þAÚ”{Úà§%�4ë‹¶–*ÖihBº1çˆ#ŽDÄt &ž%xMªÆÅ*tr˜ß1‹I?óJë+˜‹ñmæS@ÒÖó¾¾íüv÷iº{ó%³çýO êüŒi6GI�k¼æ}#MÝÕ£hÀi�Q‰g‘UGh>ƒIáô;X·3\ >Ë_¨j•¾uv¨g¼ÿÜç>wLñ˜¨¶Þ# b§Ù†°úÂ(pð4Y¸KËçµ™�o¾ #J¯Ò�0ž‡¾:G6DHÏu´Aæ³õÇ‚ݶ•((Ï»dà„ž£æÄl¢ êeî.Œ%”„\Æ”²©cž-Œßt衇n5Šfrªz‚He!´Ž™öÂ$¸ènëì$}ħ5ˆ3ê]xá@Öóžiïi*W#»rkÒ¦D6úæ·üÓÔ.ó‚°–ah !c^�uÊ_h�­¿ÃÞ_zÉtƒëß`àeЉÉ¤©p´1ìp@|¦’ÔLhU•Y%﨟¦®JØI`çƒ >‘™ÍLÙ·,K XÙtøá‡oíl#VÙª²Éˆ„ð´„í$íˆÑCf�´é�Â�óÚÖüR´mc�÷h@H87•ÔY´Eÿ¢¸Öh8çjI+í¢ „ ÛÊIã1e;™0fƒÿS¿c¾ó8h!Ë7µ þÍ̹ÆŠ„”mÄ%`á„á¬`pË!ðhXQ�÷Ž�è°¬”,›Ž>úè­mѧ5M»—aƒ¨üC6ÛÅ�tB¾�•ô"š)í�“˜)’ùNCÈŠ¸yä‘Aa6É3£®ïLf ªÕS0€iÕÒ/D1“t#j•[:^¡ÃLhKçý�»å�*©Ð~ˆ6à `‡ &#b ñ—«0P¶û±j0Úa Ѭí1cîƒ]ÂY ¥´¶³�ä” Ìæ¯Í{íµ×U� 1 Â""Ó1¤ˆÄAÊýJ¢µ«®tà‹"Õ…Ó~ç‘`æC¼G šá†�ͤ"¿E=GÃôÃÁ‚ ¼ v™df›1w¤³�ä�yÀ�pâgi¡¨®:@ü�ç;0……AH8":@Pxa¤è�ÄWG©]ŠèTä‰àmꦹ¾³:è ~&M[´‡ðb˜ ³R›ïyÏ{n!Y�À™&J$Fú;MY'½F»H„hЀU'¤ÑÁÙVÄÃÐÊŸÑByàˆn¢ Œ#åÞ3t[i2Ÿ˜Ý©ÿì´h¯ðjÛ!"" 0‹éñ<óÈ߈ØhaÓ@¾4 þ É‘•O«0!³ÓR æ{§ —ÚBX÷™gÖ&&ø®Mï`í-‰CËœmv®† LØ,`޵µÃŸpø–<ð#"BX/!* 5ZNZ‡Jг¸]u+ˆc üÎü!ÀýŽh|"‘*Äk—©›?¡Ÿ‰–ÒbZÓxŠµÇ‡ô¶:¬O¦I[Íí!DÅ}Ì„3½˜ëÂl—!†þ !�%¹à§ñpòŽöX‡‚ÆKˆÜ$¬hÌw‘�;¼Ñ& ѱ þ¢3ZpQ�*L"pú2뿲SäÞ÷¹÷Ð¤Ž¬A\öUH1;éî\Õi–ûÌ�š W.»L"R{�0[ÈÙ>'šÊɶT]fP¶š 0…iMåDi]³ìå�Wi¿¥ƒÆG�œŒÉ ؘ¿Ž1@hR�(‚ÐÕ:/°r5hÕÉÔž§yè�Ïs¾º-C ÁѤ�‘-œfNáÁ”3›,!£©ð-žV»Gðà¾ù ƒÚÒ)þ¤ªÝ~¸Î¨,âx!ù /—¨�hî“X„`; ÐLhˤS;¤MûåÃyÏÿž±Á råDè?£ÍŽ9°<@kH-Mìì‚ĨT4íó,-®´fã¿ë>žÏa Ø  ÀXLgÖh¶~øÀÓwB¢¯²}™l8ñu�ßÚ ^Â/¾©‚QðÕwçSm~Ñ‹^´Á;5’‰ÒH3�4ƒ0�#4½h[~v'¬z×™I�·+¡ì!®¥ æ‹ÖfN™sy4ÂîÓŒ±\àžg; „@ä4LJˆ|jÛ? ‰¯®‘0³üß!Y˜T…DGÄ ó>â8‚IhЪõ7¸—ÿ]Awß=ƒú 0Ì|úõ%Üö»¶½‹Ì?Ü[Ù…Úƒoó!‡²… Æ$þ�JÝBôvn—C�9�pU›¾(¢CÌÆ&L&“Š;)ˆÀ�‘ª�€ð­#!&{¶óŠ:.›öuz³w !„õÌ�€†¦øÝ»úiš¦¼<xaˆY ¸0cp®b±ß˜58—Ñ‹Áži¼á„®/Úmxa8ºz†9®`}©ÝÃ(íÀÎãÄêÝvÛm,Uø� ä4ÛþÏd�ÜÖè™�j™vÄL9|‚‘~�UÔɇ„3�#­Ô]„W=Öf �ȱc â0ƒÃ¯ñ‘`m¦LÚfr (S´ÌÛŽév³:�¿# Ç„âÜád ,xàGh™ç1“–ø.˨J2inQÀ#7ƒuLšš¦«h í„?̬úÌ»Šæ$7|ðÁ[*ú×qÆ,¦ Ñ9Ý*`!bS4¤ €–Å›ú 5Âìj%”`í°ùšèô.j_Þ©jC2dõ‰pžéø}1/L�A8Ä7Å$ŠD`fCMs Åp’\0cͦ Éí8dVlT³œ�' ¤ÒÙ|®¶½7G•žáIÈIiÚÕ0÷X”öDa4M&¨àB;ZÝX ¼¬�ß6�xâ‰[›nïÈö°�Í"¸ß6M�1•«Ä,æ¨y2¦Òs€ òFå%"¦¾<c*‰ú‹öø }ÔU�ׇIÓ¦¾,uˆÌRE ›æ÷»>›!ÁX¾S,u”yDë+‘ã¾åÌ„SÕW:è·шìÄ# þ÷­Â¿ œ…áÚ¤•ýÄ8mWnÇshÄ t8ngåðFÓB$¡$ —\kZ%bª~ªÙ~‡¤’4ÀWh Aø ˆtþÉ ¤Ø •™Â³ �íô/þ�tw¤Õ‡0SÓ:"CÂŽ;ú¿ì&H�q[Èëh:>¦v¸œxâ‰Û­zñà³­Ò8 ì�MH $;b¾=R¥.ó™hÒ%ëQdÌ„±‡¶��aTGÍYL$,�û×I›Y'æ±�á»V¡QTÇiŒiò·Õ3ì)†˜ViÁ¯mÿž­†O©È%ÇÛ˃q¤›# )ä“\BÏù¬ˆŠô˜}•Wç]Ò^î4Ò´Ý3ó‰%­0{Ž$c2FÑP‚2¿¶SÔHCTG¢ÂþLFù�y¦NKë¯j˜h!BeÎàivM*H¨‚Ìrœp cZ ŒVQ)ý<¿¡ç¦ÓN;m+ÀUÖí8¸ªEÒ* t TU²:̰™à*dA|‡60Á‚i·Mj€.²tŠ ¢tü\U!3i��OÔX+=jþºø{O^páe ]”€HL9Íë�p#:&éo­~*ð„©èS½VïÑÙ·MAñÕ£döyç�~i,ëoBG#Á‚®õ½iël}wõ:üj`pµß1„´Ìo卸Æ`Ö¢Ì#÷0~¾–A)À-ÚUé “ �¨¥þŽE¶Ý¯¼’G�0-±·Ä±èšO)ç ‚çøW gùieÏT;/ü G%G ±6¸¾…K!Üå6=Õ’kÅE€‰¯ã ùìÁ¤e¯®¾×ÀZWSG%R¬ngþ·ò$Êšf3˜?ÿ|Ÿ2rvã"©Ÿ¿7­J­®ÍvV_ ,-·Ç ù»E},úÍÅÏx¸LÂ<æ’FѤJ|óýæÇîÀõ�YëZ”)4ÍKí¢kž8åĨ%\ýîZ„YÄø]«ßï£Z^«¿õòæWÃ2ÿ®@B Ôæ:þµóÕ|Âs=h@«Î>ûìéÿ�½íˆ�ðÚB����IEND®B`‚���������������������������������������������������������������������������������pallets-eco-flask-security-72bf929/docs/_static/logo-owl-68.png�������������������������������������0000664�0000000�0000000�00000022345�15257217435�0024332�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������‰PNG  ��� IHDR���D���d���?¿–„�� �IDATxœÕÝg¬mUÕÆñu UP°ÑT°RÄÞ{Áн+jKbÁãCâý¤ÆDMl1jÔ¶ö ö^AT°`CPE/Ü7¿Éùo÷=gïsùøº’“sÏ*sŽñŒgŒ9æœc­»á /ܶÛn»M¿üå/§=÷ÜsºÉMn2ýç?ÿ™vÚi§iÛ¶mÓü±aÆñ×êó�ëúÿÂAÖ?ÿùÏCÒ}öÙgºì²Ë¦]vÙeڼ뮻NùË_¦‹.ºhúýï?mÞ¼y:à€ªtÅW Å7mÚ´æÚÿÒ º|ÿûߟ®}íkO׺ֵ¦+¯¼rè¶ñ‚ .˜>úÑ�@ýë_‡Â‹X°Ö»öÿå #ÅßùÎw¦_üâÓÎ;ï<]|ñÅb ÃÆ½÷Þ{ºüò˧OúÓÓï~÷»iãÆ�KÅ× ‡þWYÑñ•¯|ezãß8ýûßÿž.¹ä’éïÿûÀÚˆ.ÇsÌt‡;Üa€‚F«ðïõâÇ¢û�_tm½#_�g×»§óâäë_ÿúé%/yÉ´ß~ûMtÐôÏþsÚwß}CFW^yå6ÿø×¿þ5½â¯˜N;í´émo{ÛtØa‡Íäo[·nAg uêÞy–u^œÂÆùkóm ÿ]¹¶ºg>v­V^¿«�™¿çÔSO�N<ñÄé§?ýéˆÏ{Þó¦›ÝìfÓ5®q�i�=öø¯ =�¡›Þô¦Ó?þñ�é#ùÈhà¼ó΂p©eG ¥§Œ8餓F[ËÜÑ}®iƒb‹¬ÍUç�æm%rõó¹÷0’>ñ‰Oœîr—»ŒÑeŒÑÏ|§X`„á[ÿøÇ‡En}ë[O~ðƒ;YÆñ§?ýi:ÿüóǽóJŸ~úéÓg>ó™é^÷º×tË[ÞrúÔ§>5‚·¿ ÃÝO!à ÁÜÃ…Y:0.¼ðÂé†7¼áôÉO~rÈ|ûÛß~4/½ôÒàÏþóéßøÆ`ýu¯{Ý@¥î-®fãv€\ç:×™®ýë�߬MÈýèGÓoûÛi¯½ö9ŠÎ¯yÍkŽÆ¡‹IðÜç>wúö·¿=¬e(#Äo~ó›¡<Áßò–· æi›@Þ}÷Ý‡ð¢½øEQç]ÿÜç>7]ïz×? ¥= zæ”SN™ößÿé„N® ¼Ïž}öÙÃäÁÏ0¨¾x�1�CBþ¬³Îš^ýêW� Çgžyæ kœ{î¹ã€ �0è‹_üâtÛÛÞvtò‡?üaø$ÅŒXÏzÖ³&I0�êYl"È�ï|ç¨Ø%ÿá*·ºÕ­¦ÝèF#È}éK_šÞýîwO÷¸Ç= ”Ä\ÏëÏP©/qhäýÞ÷¾7~“ëä“O†Ó—ßÚ`¼g>ó™ÃHW >ùÚ×¾vÄ ¡j�ù›1œ¢”�8ä±IG”9þøã§C=t¼C9d:çœsFŽãÞ»ßýîÓ npƒÁ$}hûoûÛP˜2îãN¿þõ¯Çýèîß×b"à¹*Y�B.�W§—¶¹=Þô¦7 Ð�1\Æ]ä”þÙÏ~6|ðÁchd¡K9€¹—¥¹Ì�8苎w»ÛÝÆ=,ÍG= Lcþð…é·¸Åxõ)O1>M1?\�\“›�í‡?üáhÀú¸á“â€ÄLÏÅ:l�¾ ØŠùÚ0ÌtZgª±aëÖ­Ûθ…˜¡CŠBYƒ:Æà�Œ2&4a(#H¥YÈsαA(ê÷º ýnÜ´iÚ}·Ý¶CõÅ0”¢÷mh%#Å È~“E0fŒüÁ~0 ¨Ï°ÛÜæ6Ó}îsŸæ2†lzÕ«^µEc&$á�^(ªcÂ7t`4 ¸ñSüœoºáS»~(¸¹ÅÇ$jóæ«‚ÝÎ;OåA,ë9ñÁo€é_Œt#‘âf˜EF¬t®˜Æpþ¦´ç¸–ûõñЇ>tȼ Œp7Î,£�Rî)OyÊ@[twÍyøÖ·¾5|H�GGÞd ºùÍo>®S˜€ž˜õEA€ú7¦a~õ«_ °�…aÚpס´{´Õ° \ŒÓ†ØÅµÒƒÂâ…�ª�ieø¿÷½ï=bÑŽŽÁÖò3ïW $˜é$AÊ()¬ÓÛÝîvƒ†fîFØšv ŒÚã–ÃUV�ÁJ,òlùXƵôõ±�}lœGwàj +\׆~H[˜Å8âŸC¼q]œyÄ#±tÒºÆeúc>¸êXƒÁ’Ã?|tÈ‚èŒMBPÁ 7¾ñ�pßüæ7‡ÿ?ä!‚ €Ú%¼Üáü .ÂJè>ûÙÏå0R›”úãÿ8â˜0 ˆ#F8ýc�g¹5p7÷r÷6µ×ïÃþðq~G`¬dÎ�†¥XX¢&XrÂŽÒ×�<®‰Pò÷I™�3êüä'?™>ÿùÏ�xÂÒ|[ã^Ã7…¹�ß,^@w< �ˆ ܰú~à8úç^ä%¿v¹&fÜéNwç�*«�í2ÕùòI4Ã�ù +ðoÃî�üãéˆ#ŽtÄ ¹ƒ¡‘°”âJRu#F“:À°nâþL°\Ã> ±nY%£È-Ä'ýbé£ýèõ=g¨¦~ðƒœ.¸ð‚iß}ö¹Ž!èõµc~,aÈj€¸�kCŸa WzÀ0›Åb†ß3,²$Ëp±O|âÓÑG=Ø8 œ¶Ï=ïÜáF.#›{¹ëSäŒ3Îs!‘çø FÀ¨iÅÍ1‰kp³Y÷p_WÐ�7^ †,ž~®:0Àp\ +W ˜Ž%[¨kîðÝï~wŒ(„1”ÞñŽwÔ%€JIívèaÃò”80´ƒîæ2ï|ç;§G>ò‘ÓãÿøÁ1c¬j­Œ.‚§ψ;¥é\ ýmjÒ×LzZ’¶¯Ë�”6'�è„‚,̯¹Ä]ïz×!¤9 ¿§˜£á“‘PâG¹ €LI¬Ñ–`P€�9�yÌc€”k&€7S6:éOî®#æèƒ;~ík_ÊË‘°®Yî²x²Ãõ@ÁL§,a@cÙ)Ú£§k„FsA�r”$¡�âo“°·¾õ­œ{Þóž³øäË�ÀŠb†¼2^±ªüH_˜ @;ïnQâCJ¸Ró£–ð-cȺ€P\ŒhB…!„5RèDTØÃ6,ÄUJ– ³¨+‘c¹�¸Î)§ž2Àrp5J{Ö½ÀX� ÷Õ¯~uĪ–õOYíqýa�</¨sM Ï<GvÌÔ£–þ/ò}¬ ËC½%{tã2:�¶@‡1¬B´gi™)!t`,�Œ»î²ëhÛuîáóijdý}ýë_Ÿ¾üå/�I · Yš’Œ(­»X"`  H$só·¿ýíÓ±Ç;[�Y}´Å²�ö-Œ.þÍ èÇEø&AJ¢Rkb�x¢Sà� pÜ@;Ρ9v‰ü€$¸{¸À) ézùË_>ú°X€=#ÈŠ)²U3<�˜„�å0F?ç1ˆûßÿþ÷Ÿægøó:c÷º£Œ ’ßR\PCù†^n¡®d�Å n•£°ŒÑƒÒGyä¸FY ¸aeÂRŠkuÔQ£ßøÃ#&é�‚¶=$–cJÉ+(Ê¥ÉÃu0«$—ûï·ÿ ®å]k�锦!@y�ëü‘„•Œñc–4Rø›…,-j¯e@L(ÈRX0‚óXCyÊ�‹û`‡àèoÖŽ©~“íÏxÆømN26¥àV�Ã:çŒzûp ±ƒSœ[È´²àkñ–OjŒëXã¬kÈ¥H+j˜ÀMø??GY÷Œâm*àß�B«úÀ,áÀ³Às¦•w?”ä"ú´%�˜høË�Q�o(£¸ ð�;oôÕ«ócÒ¹�•ÃÌTÇ � ­¬MŠR [ �’\ õ±‹ðâ„gËGXWî—ß¶Ú5l;Ä!»_[Úì¼û¥ÿ0`¹ååîK€\på’ŒdrÏg·¹É< KR¡ñ,¤Q¨c`tÄ]X€îqMš�5/{ÙËFŒ±­á<ßáëKLrhÜ‹…/~ñ‹G›úЇF@‡îw¿û y(í6EFÜŠ;c„®‰QòŸÖGæX},eŒï¬#ˆ Ê-þˆÅtŠM´ÐÙTÛ¤Šõ µ‚°g¸’ïY ¢§gMÝÑœK±2°âná–�@/xÁ °€Ô_ù…v[|[ÈÄ�úv¯¾M%eëkR÷ê&ŒýÓÊ&”„§ Ö&U�SÄÜÃs¨.Ö`†6üMY›Üi‡ ‚-)²p±D  “¸î‹^ô¢Sˆ±^¹—Ö7&�QbÆ]ÃÄ–0-èoG€Ì¶!æaŒ�Œ™Ç´:ÞÚ¤†Ñ×¹¦÷⃿YiZ ÐÓÊ¢«£:@¸– ÊgmÊŽ\„Û´‚ Êp)–Æ. VÆ G!‹6†{Ѓ4®Ñ›žüä'�íŠE9Èü±&†´HÌRPŽŽ‚PQ m1Gú ÀVã)ÊB:6”b«j—å­¤D¬Á2Âb×rÝŒ™‚ÜÏH ® Fˆd�†{�ÐÉÊ� gt¬BH¶ì9÷ïèXC(Ù6w�S�b]`Q°ŒÔuÊŠÏ”5 ‰§¸ƒ¥Ñ™‹�‰Åžô¤'�~¸‘@Ê™×èÃB7q¿{´Ë�âeÛvHi×üí<™�ï9C·˜ø±cZC<Ä¢:Ñ��‹ÖmRÜy�a Ú³ óm„[“QR°î!4˹WÎ$£ŒkÜGRf¤Ò6Åj [  ¸!›fp9‰aX\ 。áÓJ®±Þ±�ie¤á„ÿô7…€ÕÌMÅ‚Ö#J¢Œâf�;*lÓ‡d‹E2ÚÇ=îqãyÁ¼ýÏPžå�\˜¡o}: ç « Óì9yZý—=s»bÜzÇš Z�¤°Ä y‹±¨øPÖÇJ˜@QŠs×ZJ+.Xæó%vžwŽð�zÔ£ÆèD)àa›vÛÁó7€°­’ njž?Ú|ÚÓž6äÇD:0䳟ýì1ÉtÛëkÒAA´³èCQ¯™¨`hjÎ:η e8v€TÈ=Zdv/WYäž{LÇwüø·ÉáYŸb„œ¿[pâ:â6´H®öhšLŠw @†¯1¨ݬÎN·Kߊ„£ÔÞð†! éÜùvÕ’ý-xî$¡3þc A(äÙienâ¼4žBÍMмY­ø!oÑ®àÌÊÚ3"Q0õeŠÀ=Ä�JJ}.Ù¢QkYÁàv» k®® †¢íŒ•„UÂ$°U ‚Ö•7aLîcÕ‹‹H¥ ɪ”w5Ê xú ì´²cßûÞw0L;¶€`Ô—ŒLÀ³Ô`BWÉ—8VsG`b2$YÜ×(3_~µ¡5?^‹Þ�fE”EAj¼ýàùEeecÀX{Õ a*v ¸‘û(Ip.ì–))ìYL?¸„9í·úÅXd!£`­q…Œrs%÷y&�ªW]ã‹ØÁ‚�•°zÑ?Ó9…1ÀÐÉï¹ zRX¬d9KZ7?i¨ö0Y[ ¥4Ð¥ì€â–Ö�K�É51 3´Ý: �r“˜Í¬Å¶– úY”“,tª ÛÄ�²E”‡t.�m?& ¿f)Ö¦r(×%fÅ×± a»ä ¸Ú',ö�Z6¨–8\X;ÕãKïw�øwë¢~Ëlse±*Fï¨îu n䇕9PÒÀh¿ÕpŠ)†9.¥S÷—Xq�P°¥„S xÖ3X‹bÔ¢�sk!P�tãÜKÊi×6)#Tm„Á؆yäùÀ>0@,랯‘]V=´�iet zVB¢&¡ý®À�Ë8в²Dˆ…«ª$ Å«;«~D»,ì>”÷caÈy²ˆ=Ü `Ü» ³žáâ÷ñ7pÜ;_Ó‚¥€­œc½c»Ò–_ã7Á�¡Aq€…šIJ… PÅb%�\ŒB„!°Ä"1À=ÚõÛ(QªÂ¸Ì¸Îóþƺê<�¨=ÏY#”Q ƒÈÓöˆy‘¾ ÝX&ŽTb‘ë,;Ö0„@¬`CXŠP²HÊhLÀ,H‰¨ 4C-�Å �¸ÃB¢|£Rk,âŽ<ƒ10§Êd �MñsÏnÇ%°„ÒΑ«En2¸_Œ„ûè àb™laT8¸ìX“˜QÀ˜oÇž¿·‡JXÖÇÖ2*°$(�Šª&r�µ_–oË�±¨9‰ & º�¹�û¸©>±€u)ÈŒ€€tNiÛ´R `1†ØÒReÕ�€ Ãz ™¹Ì|ä•®kœòè çB�6Àb1 ™]=Ú‰£¨è߬¶<¿SXP0Ú´.ÇÔ*Ë8�ÃA—«a¥ÉæðÈÚ:ŒÃ=ôâÞïxÇ;fµ)ËŽCtÆ-t$8²!«½`}ÂgmŠF1 9Àxžµ€Õû5­ˆkÃ5¬ãFܬÙk#Iôé7÷a„Ü•R Áíï–8Ä¿¹zK�ú‰UÅ¡ö—2$f %¨>œ`ЖíQ–�oǽ’«¶S¾­ôÁd­Ò,4'0Åyþ-Èú5=h>c¤ÒgA¸E+ÿv¿é¼Õ{àž8¡=#—3�@Öàõ°eqdÆ�’( ¦Ðf] Ñ€[§œXÀØôØÇ>vX0:¯ X�\ Á2÷ ²ß®{;R)Wì)Á<í“c,KXXb8?•ˆ÷þ 0J üÖn®'-X–‹Ì¢S.B@�µ mT`uÌ`‰vð¤Õq.¨Ü¶¥€frmö¾�v‚�ú¤md©†µL¶7C+¸ké‘bú¸íù0Œ¿µmZ –ä‚)] ¹mÚ—,[ ©ð¿Õ.³°ø6«P¬Äª-ÁlZyCÂß”Am` šíŒ¡0êºëîo4áíÒ»^í*ðô'¶qL"T ­l¢÷憻ï±Ç´õŠ+†Ñ<lm›LÀof½èØX@-¥´ÆX¢òK+S˜Á¢|³„³0 <kÉ_;(%ÜÏZaÉ* Y¯öŠõÝÏM«6`ñbˆö0ãôëߦúj3ý´ÓOŸ•[}kÛ¢9�Øå™i½�;V¬”@ „‡QT4ôr‹&zíêU1dEŒð¨]årE¾¦òèêhrÆ]”äJiÏŠ�òoJ°nAµ= Là–b˜g+ït¯TíÑ2¬{0]=­6ôé|éýB@‚ WuÌZX E]õâ,ÑÞ ‹¡leMý{�k~ Ð{ÁÍŽ+_àJ­uê—û¸®}B7O v ó*Œ(ï922šd ŒAioo©×ׇxƒñUK·¯¼è˜ÅŠêŒµQŠÐÛNXõ„Ç‚šC° à$imqº›ª8KP¶…*ˆ¬‰�Òý¬H9m¥7-rå–€ÈÍ©"ž¹1¶B™´vz™€ü•«“ÏòÀ¢-‰Y¨�¯ækKÁoÖ.)±ý»—yX“{µ1¥sÊ)–�Á½mDû›"âPû+ú ¨£Kè*o�( EýW£Æ]#–b­kÀžþô§€+'÷²]¼Cߨ\ñ¬¿5ˆ‚ÓÊGê­¡L耪–UìᯬG€Ê)§••y}a†xàžVƦ•òˆöO(Q± ƒí@ 7ð{9�Ûh«¬—d§ÎÑ£•<é9,Jó€E‹Î³30‡i+€%Í#Ê&�ÀŠ�ë5OÖ뜵Tbq)7ðü€{í½×�ŠŸ÷®¬k,[u4«I़¶«AчdŠR:’Íp÷�²í�gË�âˆçéh“ÀVñ�l^CVF[ã2EZÁQé¢Î FÈ” ­h¡-÷ (åPß3ü˜Õ➀+p_2®¢•ûÖ=ÜßZ*%=K9ÊœtòI£/Jk V뜫”B½ìȈ­Ñ hbSûà TmÛ¢cæ2ò •hvíä„r­ÒKJ´èC8�µ(nPÙ%�;´ÑË>½9h}3à�§È!2‹Õ—]:½ùÍo†3zèhŒØÝVÞÛÓ—4Ÿ1Í�ÚάÜ*Ã2äꊢíâw³R¾Yu2ÿ@Ëö”q­�d×zMC’!{º•j³ �`b zÿ–ðb …œ/ðUÈHî%Ó‰'œ86¢Ô�ä@à>,.&ôFg[–@Ã50Æ"·ñì¢ø1‚ê±Ç;&w¬ÉúPE=ŠI’tàÅ5Lh´Œ€&ûÈ,æH’PŸåŒXÐÞr¤¶0O°¤¬Œ“ë§—˜õƒ¥®³6 ´/ 4:è�€¥ÚÄíûÁ°*¤Å@Ù5½è„¹±È¹è˜¹ !Y%:2ŠPÒÞhd5‚E?”ù´H.9"¶´É:\Ïy�˜â³¶ç� €^ˆî\øÖzïp®—,²xÿ£}ÏIé]3ºÉ9ÈI�ö&Xe¦kF™’ ÿÛJ —`A¨j\§aÍ hX¬œÃsj5°ÁK­^O»í·Rª×bËhÛúh±G»Þíç6õWœè}Lv/r?e{Å«€í^×Å'ÿæÆ2îä]ã2[¶lÙÒz£›çË 4ØÚ¡d‰iQ‡/¢¢ØC™&m\]uj(,óLû;€Æà(•|ówi�šË˜[¡9¹Ú–$'Ö`(@�£ÆËºѹ¾iB7¬—,bÇ �Q·áªyF‹2-6—e,¡7VÆH�¹A;ù„à>Š`(Ã_1ɤ‹¢@Z£s³fÖÄ%O½�- ¸dÀ Ÿ˜`Ìë pYìqÇ7·UèJ%@l¡‹üjÙ1bH´móÝüÍ‚¨ÊeøjKpXA�‚ T@lÇÌó-뙯 4Æ´`D¬³6ážw½ë]ã9 c�@Ίúî¥F Œ1 âE[&\˜âÕX ‰_®U†Q¡Ïžð„YyÖ¢cTÑŠ€JœLÛÊjˆ �vxéP€uN\ xo=ò¬Ö âæ˜Û´¿#NQkÔƒyÙEcJçY”e±ƒ›­³vá0ÜÚµ÷~¹ª¯Õßü§„³5Us²ë` pšhA^ã”Eíödý(Œ%˜ (qG4çR˜Åj­�•Ë‚\ÏQ½ 9<‹‘}̘ØU}*&œ<Œa,²TÃ`ÍÓ<ï|k+ËŽí1‚fŠe—�Ñ;m¹—±e( óLA5!{«ÚPž�Î÷e�Q Û‚®àÛkê\ 3z}µüXb–aÀÚ!äÆF‰¤¶0ŠŒúÙ˜g€*´§%+fÛ’²~ŠðFßÞTb¥¶ ±‰póù%°Ã²#E(^¦Øç5ø5Åù}™o;wÚ·Ü£�ÞÃëýÜòýÚ�çÂVÑÚÖ.¶‡Úxï{ß;Û\ïØ<�V7£�c¨Ã¦$+õžÞu#0«ldhñ[°ˆ�msö!•&{F#ýJ±Èh –è�;ŠM›wÚi(EAn[àÅlné`÷»;K榕â^íX´„8Û†h—«e;�©e1–Eµ2L÷«D.!ã›b Ød»Úàï€#Z‘ä{Þóžá&Î÷j7Ž�mâ ~S@ uj[·Îö‚õUÉ9Ð{;ª£ǰnÓ̰Œq½¦ºÞ±Ýf÷|…M»[}·Ãœ‚¬ß§3ø<‹#Ú,á1�¥°‡’,f´ð,p)Yu1àXí Î-X·ùб©å„^Pð|‹Ø¬dìF{ЀÆr?ŒÒÖDzcÈê×®*!ðÂ¥Ùu†Õ�Šh;_á×GNø;¡¹Œç¸T�píWÔÒ¤Îó”·—Â�”p¾XCŽÓÏ8}:ë̳fEÿÅ™Þß!§!Ÿ�Ä,nj‘«, ªó0¬\K®|[ Ò– «2?Ëp"¾NX›¯ZIësîoÝDæZ¡Lk·@  6ÜrÕ¾`Å€×׬Ï`àA4úà.¥ðþæ–�ìEIù 9*Ñìu•eÇHÝ狎© I ){+Á®�(O þ°¶Ërëœ`X�-@0¥Uu  F4/V™`Vâ�AX‚�½+Óò`ÜÇ5ô×;~²Sƒ@Yt/5,;6¶œ?ÿÕ„>2‹f6²+Ù&¬4™u}uRúM–`Eî‘Z n{´uTÂkµ�\§7¬Zu£0™ÅÊVñ�[�¸OR’\”4…ÀŒ6ÖeÚ}ž§oôõˆE/1ÏBÆå—_¾­]ûeÕ½EC£xlàãØ€®�1õÇ$ñ‚+õA¸’; ·aÎ…¦•Oq8LƸ^umÀnß…¢@jésÉÏþóƒú„!˜ka/·ÍmGÃo««=c (Û¥ksY\ï¨4’T|‡ jQ #Õg-©>ËO+;te¬½±éõý-XöƒÞ¤ì‹š”2äS¬JÂJ%(ÓK ­Ä‰k½âMNF!oE1}ÆkZ•�cÓkl^=ç9ϣ̼_E¯Ê¦ç×àœm‚ ˜A}ç�£Xk§ÎñiQ߈”Rþ�]”q¿àíÙfÙÕ“õwã6ÓÊ‹‘bÅ+_ùÊÁk%ör+ü/¾ô½�Õ‹A«™`^0^68ûì³·µ%©Ck˜À@2)ÒàüAؾd5­|É–Òí¯ƒïc�¹ŽRÒf2g8€¡‚ž@�9úµoÒ—&¸Dk”gu®kfL¶6Çæ'm•v²-m¬f¿¾äJÀÔ·dq¼˜ ï+1¶õ =ÖíQ—ÜÁ}Ý>³ã¾§>õ©£ïì»G‘[ðeïÛz­[K­”Û„ XFžÊ2)K‚zÞ#�e‰�� IDAT€KÇÝÓ÷XYX�É­�<2™ÛTÆY)yïÌ8'6Ù‹Š±\~ÃÑG½­�¸Q#ž‡¥5 Z˜u{Ý£mÈÞÆ ”sjCV+»…ºg¸–߬ޅí§ôÍÖ˜ûP­xSÐ¥ˆçõ)Ï�–>ZëȽ=+®`©vµÑG¢ Íí›ÁÁ+'‚¶R� ÇsÌ6º‘h8ü ((î:—í±¦—�+¡t¤�§<¥ ‡}Ý®g¹b«`è³Ë0îårÜ­O_ò¹7Ô&ù Ùò¥¾¶IþJ>¹eß< nŽ5X¤Oíyå,›Ž:ꨱ„H8Töªº V—*mhæ‹ •"è@§®nÛ2DãÖ?¹„I‹XaÃж¬-–oßH�@eþ݇¤)æž^V¢ W‘ë§�5ÏWÚÙ‡ZÆmÏhâý“ç}ï{ßÐ 0›Þÿþ÷oiúOþ²b›ÉÕ†éÐo?eŒ£4%€¨�#Ð_ÌpN‡†H ±ÞÓØP ØŠâPÚ‚´éëy–K¡¾glMx®i?È,Ž4Ãõ,�€G Ãh؇¸7"¿¥Qç_÷º×M›Ž8âˆ-”%[¬Á¯ä³hJ³H™`� p†[s@Ô“·`,¸º.e¨ûPŸî¡¼Ÿ>újsIâç9Ñ3Y®�9}ö�4ÖçBmyŽ·ÁUËp˜«rU÷q·Öxõ9^‘=üð÷˜ ¢K_Ø­¦TÖ6Ê4Âj½BÆg+?0Ú°2V±„h.ȱp_¬:�($�ÈÓJ%#…±C›¨Þ×®$})ÆP‹¥\ŠRÞšG+j\�üÒ�1£2}VÌm°UÜ`<k±­ömö�Ê�Њà"`óù.IÓpó JêLŒñLoH^Õ\Æó }¬W©Tõ¨ú©b©WD0¤ï™h£‚ôMì­Ü´—¯�3B14pÝßj]ïÓ�[c&ýCŽ<òÈ-:e ÙgAÒofKpB›zS ú¾]êù¦î½c[å EÌ{ÚW‰¢�jÿÕÝ—òÚë­‚H¼Õþþ‚jYµAyì%§úY÷ žÎµlØÚMï�‘G|j ^øÂnc] ô* bKU�Podèm)>«1”(îÓë£}Íïóù÷9çL¯yÍkF?\° 0F0ÛÈrÏK_úÒá˜B_óÕ§\íýÛŒ›»û\:YÄàéœçªŒˆ¹Ñ‚¸¶{W¯å„±n+ZS(훢hƒï]Ø^UG˾Uê9& ¶lÙ2u¾rk±Ä|ƒ`’ž†mç[ nlÃÜÿLÂ…ûPd4ý’ÃÒ!{–'1˜ ÂFÖ�Ý–ÈHqm‘A?<ÆjW�Œc´]6ÛÕhoC²0�ÑJ-ìôªªh]ebuó€1uúÆX _@¬wÌ¿W»aî#Ö‚ª¶þÇÜäÊmÓ†�ÿ�°¥(÷ìm/|ûJVߤ¯2ŠGŒuÜmWkf�5,N4u¾:Gïì9*úï}ÙÞÙï#jöïÕõ¢ó‹Uó_”êܲπÖβ5S£¤øbÔáJX¸ÏXäºè¢éÿ�Ò˜�\TÀa�����IEND®B`‚�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������pallets-eco-flask-security-72bf929/docs/_static/logo-owl-full-240.png�������������������������������0000664�0000000�0000000�00000205337�15257217435�0025346�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������‰PNG  ��� IHDR��À���ð���ÏD «�� �IDATxœìݼžuyÿñû ­´­£-‚Ömµ ¥V”½ {ö†FØ�°�a„�=7 ˆTë¨"ÕŠ[Eqv˜ü_ïëÿ\éÓäd�sŸäÎõ}½ž×9çyîû·î“ñù}¯ëúõÌ›7ï‡sçÎýËßþö·¿š;wnÓÓÓÓüò—¿lÖ\s͆î½÷Þæw¿û]³Ã;4ÿýßÿݬ´ÒJ�ëè�þè�šyóæ5+J9^¯Òà–gäwÅ3{ÉK^ßÿáˆï»•ϲë÷jÕ¦i>ÙÓÓóázÄ¥R©T*•J¥áªbžv´2À°˜/{ÙËVýÖ·¾ÕüéŸþi€îo~ó›æÏþìÏšµÖZ«¹ÿþû›Ÿþô§Í«_ýêèôÙgŸmV[mµfõÕW_¡ƒO˜J¸* ^y>ž“çõè£�6?þñ�›-·Ürþd¿o¾_yå•ãë›+Z�¶T*•J¥R©T*õW+uƒã[ßúÖæ?øAó³Ÿý,à÷ꫯÇ÷�ÿø�›;î¸#®ù÷ÿ÷æÛßþvkðÛ×� ãNX*ø¼J‡Þ¦ÊSO=›'O<ñDüq€›üþÇüGül£Åï_©T*•J¥R©T*µ­•²½ kþË¿üËæ‘G‰ï_óš×4�?þxó�| yæ™g⽯~õ«ÍoûÛø¾?à™àšTzq*G�/°ýýïß|ó›ßl¶Új«f•UV™¿¯|å+ãgQk¬±Fmj”J¥R©T*•J¥Ö5€9½ÿó?ÿ!Ï#GŽlî¾ûîfÓM7m¾ûÝï´dh´k„§¶!¹Æ¿øÅ/–ÙÎpÚÒà“�”„W¿+¹±bå½ï}oóÀ4ïz×»š·¿ýíáêSŸjÞð†7ÄïÔ¯~õ«ˆ6ð*•J¥R©T*•J¥`0ù /DHñg>ó™æÌþçþgóž÷¼';ðòÅ/~1àdÁÂE}0ZuÕU#—8!©4ôåwCÑ4›�ØÏ¢^ûÚ×6ÿò/ÿÒüüç?oþñÿ±™2eJó¹Ï}.¢ l°p�_þò—7M%ø—J¥R©T*•J¥ÒÊÙ,XùÎw¾Ó|ò“Ÿl¾öµ¯5o~ó›ãëÉ'ŸÜ\zé¥ÍO~ò“�bzÝë^×ïÑdA¤¾Þ[\ê~& ÷¿þ뿚µ×^;6Vä•¿ï}ïk®¼òÊp}¿ò•¯4ãÇ�oÞÿþ÷GÎï�~ô£æ/þâ/âwÐïØK_úÒØŒ©0èR©T*•J¥R©Ô¦€¹½¯xÅ+š?ù“?i;ì°æ _øB|•³é=ð†ßò–·$·]Z: …�o.¾0zN®¼^¿+$Äù�o|csÒI'5×_}sÑE5‡~x䑟qÆÁo¼qó½ï}/ ¬¹¶68J¥R©T*•J¥Ò@(B � È»àeà 7l®ºêªæU¯zUäsé8Â_ÿú×›O|âq]iùi0a3ó;$”Y˜³œ^ïßwß}QTíÄOŒÈU ÷Øc�fÖ¬YÍW\¹æª‰û�U�~¹¿`:ÃâË.•J¥R©T*•Jmi~¬„¬?ÿó?ož|òÉæ¯ÿú¯R�t×]w5ßøÆ7šÇ{¬yî¹çœ\ JøUÀ äÊ!0iÒ¤pt'L˜Ð<ôÐCÍÇ?þñøÚ~ûí#Š@nù›Þô¦ùG=ÿüó½ Zøs©T*•J¥R©T*µ­ù� dÒV˜HõgůƎ0üàƒœ]0¥ödÝUØJng÷ù˾::kƒ 6hþíßþ-\\Óž~úéx�vÚi }ÔQG5»í¶[ó¶·½-~¾~×T·ñ⤬��2]*•J¥R©T*•ÚRÏܹs_hšfÕîö:n¿ýöæÙgŸm¶Ùf›æ„N(v„ ×nêÔ©óÃU Rú'�©�çs(­eŽõKO|©yýë^@+ÄÙ9¾ë¯¿~äõÊ 1�z·ÜrË8NË&аgÑ*B µ_ü?ÖÓÓ³ÞBï–J¥R©T*•JÃDÅ\íh¥ÞZIÙvÛm£H·NEßQ£FÅ7÷ÜsOóÙÏ~6®þº4Òf÷±mk0þB,í˜\'lx(ýRgŽ® Ï«¼t•8>Kîø1Ç3?Ÿ×|8Û X°JÏ™ïûÿðÍŽ;î°\y¾¥R©T*•J¥RiyhåîPÖneåá¿û»¿ pQúûßÿ~o^„¼þâ¿wØqF o½µå³_ÿú×ñ•ØÛ5ýUÚ4.¾œ ܲ­E­ÍÒª¿óÔ¿c�8îœÓ6ÖmqÏiY”GX`›"Ó¦Mk®¹æš�Ööç?ÿùfÝu×��fUÄ¿ô¥/… ~�÷Ëñvð_ÜX†BìR©T*•J¥R©44´xY`$˜¼þõ¯oÞýîw‡¼æškØ8"éÆol®¾úêæ—¿üeóÃþp~8t÷+Å Ô×@H?Âiù¢æ²(¹Þ9´?üðüâKÞ� ­/œ÷Û,èm-§ÌÅNqØ—æ¼äŵß�OëyV¹!á¹Èí^Üý ¶ååùÏ™3' ¥í¿ÿþ‘ßËÍ=óÌ3p¹r0 ñ6Ql–¨ü æ­õ’àwIó*•J¥R©T*•J¥eÑÊ‹ aN÷íÛßþvämn·ývQ!DýÓ?ýSsÁÜÜyç�ȪDƒš·¿ýíqü °j:n¡÷õ ýÜ_2«û ~µ ¬–V VÿüÏÿá¹ 5�{ãÓ.@ãxs…hê‹+lcÀ½.¬Íe™·kÊ—t_~®òòj«­ùÙÝEªŒÛ3ü×ý׿¯þꯚUWýÿiß ßkqG[uC(¨uÔш#š3fÄï�! Ú³°yÕ °gx¹Zš9”J¥R©T*•J¥Ò@j¥nPZøÈÛtÌÍ+_ñÊ�:a­Ša]yå•[œN`,_XÈ4ðã,6 ɀ˙¯ ­ýUÂôý÷ß¡µ )-n.M/•…AÛ©§žÚŒ7.€-AÑ :¶§{ƒ ÓÚE)Ûyå+_×'—Ù½Kèeq‹¼° C¶ÙÐÝÖ§?ýé¨â­HÈ÷º«~{6Ý€¿àËø›N¸³s AôøñãÃÑ¿üòËÃy׿�Ÿ[Smú½þÜtå —J¥R©T*•J¥ÒŠÒJ‹ƒ±·o�uÖ r½3[¢ÂX øÕ¯~uTˆ"ýÄOXFq¤UV‰#”ÒmlËeÀZß¾Ç;Þ±Äö„ßx \kssO~xtÅB½ýì Û¼�Ûìµ$Hå~Þ)S¦„ËlàÞ{ï�Í�Źî‹Ò¢À8ßÿêW¿U»3ÔÝ>wÞ™¼\_/‹Z§l߸A­yfŽ/€>î¸ãÂÿò—¿ ={öìæ¾ûî‹ç°ÝvÛÅü„˃a°íÙ§ëÝÛØ—F‹k©T*•J¥R©T*-«–ŠÄät‚¡Ç¼ùÉO~a­�Ï÷�åÇ?þq8|_øÂš-¶Ø"rC/»ì²€/à“ᵋ‚¸¥Q·;«Ý™3g6ë­·^³é¦›ÆÝÜF�ÙÝ~^Ÿæ3Ï<°¦Z±ûò:÷Üsç_/DøÂ /lvÞyçù.°Ê×]tÑü>œ�Ìñ^œr ;ì°C¸ÓÖËêo÷Ýw8Ìqó’”®qºêùsö#äØš€P¡ç)ç5«À¼ß~ûÅ&Á¢rns�l*ØÜ0>÷*dØÍ_Õo¬žz꩸†Ëë÷ÁÆÇÍ7ßëÊå=ztä¿öµ¯�ú\*•J¥R©T*•JƒA½ž¼ Ñõ×_ßqÄÍ!‡'$�%¡ÇŸï¸ãŽ�uv0@•/ú·û·QüˆÛÊI|\×ÌmíVopÖ ¾Ä™½å–[¼Òéâ`LujyÀLŠ0 Ï~á…Ì}ôÑ�O×s(S‡A;GSA§o}ë[‘ë*4ú¤“Nа_Ÿï½÷ÞÍßüÍßüŸñõ¶^òb�œv_k]Œ—]viöÙgŸ…æ›Ê�ᜣµ}ë[߯”µüâ¿–8à€(lå>Ũnºé¦æ ƒj.¹ä’fß}÷]äXÁ¼{¬ÀÁr·ÍÛWN¹ �ÿæ7¿9r~9üÜë#ßÛýwÝuWœéË9v¼vM†–÷U�ç_ç�—J¥R©T*•†µª8l;Z"�g±)@%Ä•sÊé½oxÚ7½éMz+ß•ËzÁØ÷¾÷½¸çºë®‹ià8½ÿ–·¼%ÜQÎ0�¬€Znñ¢ž9œ€Lˆ2°6.È}\ýõÒ8®Ž2úÊW¾ ~󺧟~º9ôÐC›�vÚ)Üà÷¾÷½áV‚Tc5ïq~Á2p?ì°Ã"¼HN˜0¡ùû¿ÿûùëÒÛ/¡ •±�º Žé±Ç`ý¶·½-®Q, 0+‡vƒ 6p Â””ì»ï¾»¹ôÒKçbŒÜY}Yk'ÝøÍ…+ËùÕw÷x»s}›N¸³—÷´gGU‹pvN/P·i èÕÑGùàª=ë˺~øÃŽçÇ—¼ÑFÅ{Fm;+�.•J¥R©T* wõÆ¥eט»'Ü™› €Á/Àän½õÖ¼ bù,‹Rqx·Új«pyAЬY³â˜!î©°ØóÏ??À¸ºV[ÜDáÆŠYqg�Àæ:. dÇŒ×�GN³Oà@~îsŸ GÒXÁÛ^{íÐ ��üXΫâV®Q iìØ±1ÐÆåu\�ùqŽo¸á†f×]w�°èN8!�Øü¹ÌœMr~0p46Ÿ ”6ÌŸûª=®ùyç�ý¸†Kk  68m|èC X—@ÙØ„o¹å–Í‘GPï™×–[lãÁokf]l(�|°*w;e�ÜûŠW¼"�Úø´Ãéµ9`à�ï|gäúr•�Ñ\�_6 òLhëi#Â1Hž­u±9` Ÿ¹Àý=G¹)�.•J¥R©T*•BÀíh‰�Üt[.àTàø]|ñÅ^ òÖ[om¦M›À°@0Q ×Ù³€Ma,¹»`Œ;êgÐëN1@Z ¤q—¹¢ —cÌUË\d`Ì­qúÛ�Sø-àk`Úûrcåþ¾æ5¯ æXºø�K¡ÑæîäÓÊwåÌ‚lP?yòä�ÆÍ7ß<B}�¶¹£9n÷¸ÎûÖ àê ˆƒîí·ß>€èjÇøA6˜fìg�k#Á×�7Þ8�Ø+$f¬œwpé*c5Fãâ~ƒEké:cTÄËšRV¡¶–Â�õ§-óÑ6 lhêùÈGÊ�×újóãž§õÇÖñ¶Ûn‹ .½±rË�£ �—J¥R©T*•JÀmi©˜k ‚€’üX®-SàI¸°ÜQ9Ÿ •s �A‘ð]ßs�%HæŽr …)ƒ>áÐ*¿~ðƒñðmªk<M¹µ™¬]� …â2*G@+'Œap-—� ëõ@׎±sNåÔ‚IsûàÕ|šN¬ÌƒÎl\Æ©cÖ–ùRn*`ßÚn:¬ðdÅÌh¿£‰€6(Ê ö�ÄsƒÝ4�8�ÖŽ1‚caçæ«MPjNà\³õul7Ûx„¯Ë—æhÛœ�»6+l�j÷dú1O› Ôæ�ÊÏÖPÈø9çœcð\díO?ýôx~S§N���„òþ¨�¸T*•J¥R©T*�nKKÀMׂï¹çž*,Ü×q8à   £Ut‰sˆÀ$0æ¸rl¹žgœqF�š£�„É‚*×�RàÇ¡¼ñÆ�áG?úÑpŽAG6áËÀO•b!»ŠR�-`kl`Nûà9�í¥�Ð8Á«6Á(¸UÀ @Êõ²,ôðGcf �“k l¹¯îåÌ~üÁ�7¯[óu‡À[8´ÜháÆÚp4�ñ�Póh:ެ¶@ýÇ>ö±¸7Cž�?þø˜€z­-  ’¹ÊXk:}f}­# åD»_>0çÖÆ€×UW]�Î)w�üfîs†LÛ„°^ž³5ÞÖ…‹ ¢m@£ùjƒ[.ÄZø6ð·î6FN<ñÄx®M/E·ú¢àR©T*•J¥R©�¸-½ä”SNß4ÍK—¦=‹2�ØVÀˆºÉ&›„;ªHhñË ÊW©š²JÒ@ 8s0¹š€ r�µÅœ�d/Å«Ž:묳ÂY~àM[ÀU>¯ðhý AæÄºGˆ¯"QÆfü� r=Á4ÇXè¯yø\µjΧûw¯@V³{8›àUh±ñsmŸþÎÓÒ,ìW¨µë@©c�ªRÀ *�f¼ßyú;Í�~ø£pa½¸Ä¾A=÷–Sd�®6sNàù˜cމëý <�[Ø·¹s²åc{/7À-x6æhí¬¯5Žn³Ásó<µ ²¹Ú|p<¡Í  s„}ÑÆn=¹ÕÚô̹íœä6Ïïíééy¶§§gæB”J¥R©T*•JÃD§�vZ=ê´Ô� œ@+ð‚Ë��0DU‚¹Œ ME`î§ïû“Î+·“;«h·p‚1áÅœÝ|àás/Á›üT•¤9´à`‚NÎ.§à Ù庺V{À *²Å¡ë`ÑNœS¬¹ úÐ6GÛܲ²10ûÆ :ÍÓµò …‚ƒJÀp�«ö­‹¹ (ªÆ9»ÂÅÁôZo\+éŸýügØ@¬rt­ðýÍo›\kÀ©mklåÙ Uæ kÛ³�¦Âº9¹à›sm]À+ðÖ6Ý:»žïYÙdð<¬…÷ŒÓÆ�0l› æàùZs›œq÷s˜A²¾¸üÚò™gœÕ®ÛRp©T*•J¥Ri¸«�¸-u0�# ÅaT´Šë…ö*(ŽÍêÏ�@�`ÅŸ@F�䘹¤ ƒG¡¿@ì?a¸�xöìÙqôVÕa §mŸH ÇivV/�åÞj{›m·mÞñö·‡+)„XØ/�ÛvÛmítlˆ6/áÛú‚ î�WN+0šœYÐ-7Ø�¥±kÛ�À<#èæ¾®ÓÇ5 \™#××X@­÷­§9ê—£ Ö›Îy¿òvåØš»ç ¡ÉÂÀ¹³`�Sìø¡t�Í  ÊÝU”‹, Ýü€¼q9;Ù¸„<ƒ�kj®¾‚ãÌWö¹éÜ|ëh­À±ùäqYÆÝ½¨ã¢–V]*•J¥R©T*Ut[Z¢œ ÍIJ Oq)N- (òi�SÜC×r¹©Â�ˆwà \H0åœY÷»Ç1?ŽÓáà ˤ šû)7X›Úáš ]æ: ÙÓBx3§×Ø�9÷ô3ŸþtT­6pª:²_aÌÆ¢}ß«ÞÌ%•X0ª-plŽŽ=¦én Qã ëî¥Æ�ëæ>cºbׂDóÑ—µQUÙÌÉf˜ä¶ eú-™Û LŸj®_ãà@[s›  Ÿ»Ë¼BÂý<zôè¸×úYsóà`ëÜjãΠ�³kæ”Õµ=7Žòøñãcóã‚ .h÷¾¾�É&„ùpŒÝ¿àyÃýár€K¥R©T*•JÃ]å�·£°(&éâ7€&�¯H ��'.!°_@€‚R.$G”rA›Ð]%äº\êGA+¹³BpçãAs'¹ÀÀ�Û L9ŸyWÓ88ËBŸÁ´<e×yÏWà,OúÙWg<ÜdG÷¸Æ¸A((—¯ (9¡ ÕµæT}ð9¸úL¸íÎ}õ³M¡Û 4×9×�`æ‘Q€Ú™ºŠ�YOíÜsÏ=q­v¬—ÙÚsjÍE^/7ØØ�÷ÜsÏ�c£vÞyç‘6¶|>@\N/àæ¼Yð«}ЮÀWwúôé1&s·6)äd+¬åØ+áã\þÜ��銠 #·žà·7åïQ©T*•J¥R©T*­(½d„ ãÿû߇œŠS€-ÃpÁ—�“ü䳂Q šŽ°ÜP�f’L@ lÁè…B”9° \ +æ�@N"€¢ ”åî‚D9®@ä�@€æÈ@šñO»|ZäËÙu_�©@”œX¹¾ú匪0 àŒÇ8„Qƒ9c�\RpνšúxÀD»GŸ�2á.Þ­çÛÜŒ%,�}†�k3C§�ËÜÁ±5ÌÜ].³è²B̹²Æ&oZ·�Ï ·þcÇŽ ç—»‚mDp•mÈÃæ_z饱66 8ÍdÅ„¤ÛÔ°6@šÛk3Âó÷Œ=wðm­²"woÊ£«ú¢r€K¥R©T*•JÃ]å�·£—œzê©ãŸ{î¹—9àÕ-€â�žXÜV¡ºYAû*Ô±8B޹ˆŠ^e-xg®Z L¸ðĉ#O•„3ƒcN1ऎÿ£�V±Pg.*XTl ÔF` ¸· TÅe…²ÜÌ|&_ÀŸÐa!Ï`�+ Ø@;€ÕžÐcŸ;7fp'Ï×�·µ0Nài ¬›�kØ zæbóa²‰ÀEÊÀ ûœ³ë{ïë_xVÂ6‡Ë/¿< ×8ŒG›m¶YäþzåfW˜ƒ>f̘/÷Zû]tQ´«=kü?üð˜�v¬ƒÐp/ž»ðjsU  l›—çdr¹­ˆá6)�qÓÒñGÝ*�.•J¥R©T* w�·£°+#ëßwŸ[ë+ØãÊ‘•ÇÊÅŒ»í¶[3iÒ¤�MФ¢°ë�(SŒÊFàJ¨4¸s=ËÊÊ ;"hêÔ©‘S¬‚2'Ôç@Z¨ïqÇðà´ ¦…ã?ÐÇÅ‹€™£ úmò Ãe­�ž6ÞxãùG;U®(ˆÔ—ë8Øæ-\è�Î<‰l Xól%ÏbZ€°6�³Á½±[_´8ÄúãÚri�<×è›;gØœl"Ìœ93æ/ÜDç‘GÆ ZÁ3@öƤ�–Í aÌ>ß}÷ÝçWÖ>öØcc³Â†„öÈ3Õ§—1˜�k ›6mZ3/N/÷\¾¯5â-ܺ�ø-•J¥R©T*•J¥¶E°VZi¥ÿcýfè3ˆâä‚@àÄö ÇBrAç–S ¢×±Vœ « 4U8-§Øy Ãu?·“ƒ¨]!Ër‹9‹sÿ07@ üLýF.$¸Ú{ï½*9¦Â©¶ï�EbG�bÐ)l„æ™À�œ{lœàÝø‚ò>'T?Æ¢_àŒ³bµþÁ®1�N k]¼À ¨ÆÖÅø¬ˆå¸Z#/ n]µiÜæœÇ(ùܽ€3ó˜¬{€ª‚` T9ÉÆ�5ô•;®-0¬Os²Á -�ϱ÷¹¼_° ú=Ÿ<0ú2Ï|0æ ZŸŽª-L]a1!ÏÂÜ…Wëˆ;ʳ5Õ©ýnd°¶Up©T*•J¥Ri¸«àvÔkh %ÇÈ�Ô¬à›/ÐÃiôY†Ms§T±$ðÌ…J«U„WÈq A. �` €‚V°UyF̹å�Êq¶ ˆéŒjè‚R-tX û5F ÐA®ÜUU¨I›ÆÂaÖ&(6cjÌÕ àÐü€> Ô`ÞÞ7n  Nõªs�Œ�Ãj½ls3ÁµÚQ¨Šã ̹Û@šî{…­¼äîêØäæ§Üç<›${&Ž“²¹�¬9ÁY�ÛÝ#´Üôí‘ÏlV_¶¾�šc†µÉaö|l§6÷ɾöÚkÖ·Ý�ݶ €K¥R©T*•JÃ]Àí¨W�n:ÅœÌ Nq=ÁVwÑ,¡¼  ¸�/9¶À‰kì€X`ŠÓ`�'‡Ä�UðÉÙ¦RÐ ,¹�™ ¬�¶¶„K ½æ 9¬àŒ«»Å[DÈ®~åkaÆ€œ‚[… „;ã–k p��ó¬-Ðj.\ZàªoÀ `9Ú\V Vó ]k9¬`X;ÚÐP»ÖʺG»ÖˆCë¥ú³52f®³k¼/ôÜ\€»¹[?áÉ·Ýv[äñ e¶qanyœ“¯'žxb³×^{ÅÙÇÆìëçëcÓAEm€m-=ׇ?ñp\£B¶ÐmááÎD¶až‘5¿æ2�¡ÏÀ¥R©T*•J¥á®àv´H�^\Å^ð×]íØÏ€Q8­bJ_P˜T_lÀuÝu×�ªÑyv0g@R¡¹ ×ý®¦Ë5‘\Jp*טÊæf‚I¢8·œX�é^y³Îïåº æÐrB…?ë[q)Ç…Å@�ëÇgò|�×€±�Pã0g°+_ô�RóPd Tr�O”ÅÁ€-ظ€¨ùºžƒìz×™ƒ¹qº½âåù\P*¤Û1OÀØëS1/ æg^®åÄWk¯r6ÈV-Zø¹��ÅÁŒEuháâàÛ…Mmx.à×õyޝ56Wk«O×Y€m~ž§ùZß�Tp©T*•J¥Ri¸«�¸-€—V Ê\Jn(�{\Tðäèp',ð�A.+g8ù^8sñš�¢ðhî$ØWPëìa0ÆQÕÀ:-÷WwÜqÇüŠÎY°JAž1€K®/èæ¸Hà&Á»^æcüî7� *µÉåîƒÏ¸¢Úñ¾°b õÅiÖh4v� ®9¸™ïËñöµ^`W7§–û«/kxê©§F³â^Þ®æ.?×ü8Ò�@ Ë–¯ Ê�›# äåõêÜê�ÛË=òY}Úš[oí[_Y0ŒómLæÊ‘ßu×]ãzï ?_܆IU�\*•J¥R©Tî*�nGý`ÐÓ]ø &ÔÉ'åZÊ/ŒÂq'וsv�g’«Ê±•»WÞ@Ê�� �IDATËy¼Ú{Âs¹“ÜO¦ðjÐéZ..ÀÔ?�k\ÐóÎ;¯Ùzë­ÖôK®;ꨣb<`¤i})âÀ½ï~ã7&ãx ’s RõŸgüšHôž¹ë#sˆ�I(³ÏŒ•sk<Î+Ö>(Íüj¡Ë\\�Ë…åÜßgs©�¿Ìy›“'OŽ"U>ž¬]}¤£lÍ�-8v¾¯9ÙTøÄ'>޼�� à9ž¡µð�Z6 8æ\hîï}÷Ý›6 <WN<�ßæ�¡àR©T*•J¥ÒpWp;ê3�§ã'Ô:Àšá¿àHñ(Ž"pf\_` 0È ,ì9«)ƒ9À¬8Xåì4@ A+§8ƒSàÌÙÔ¿>¸·^N*PËÎ(μ_./˜Ó`� Êqu= •Ãll�?þx̾àÈrƒåÀ‡×XŒÙ|UR6gg s[Á%v�1™ŸvÀ©sz­™1Ý~ûíÎsæÌ™_õtrq÷ûßż­!xë>7@Ëu¶&ÂÍ9éBŸµí}αµ6FÏåÊ+¯Œ¼b�k¼Æ¥H˜âXª:;ºJ%iko.æÍ¡7'®¼� ®³¯ Y; 7Ï>H�—J¥R©T*•†» €ÛÑRp�”nowÑ#N&ò>wÓ×Ì3ŒN!ÅîÙgŸ}n9�W0æþ�©œÞ<âG¡%€�`Îà 7Ü0ÜGn%@ΠV@V¿€ Ü ˆÂ‚õáµ#ÿ×µcÇŽ�ðeŶÀ¦ð^àÊa5`àA¨±š£Ï­¾Ýç{ý€nó×¶¯ PºD+ú%ßÙ:¨äl<@Z¨¶5³Îúø×_}„•'ür`�û / ðÑ>׿Í!áîׇû³Ø—ûœ m€páßž�*Ðîwþ¯9û̺;k™ÛlSÀf‡¹r‹Í³YNçþ�—J¥R©T*•†» €ÛÑÊËÒJV}¬� >Á^º�� p=óÌ3±Âz9¢®˜*s%¹”\E.¨Ï´²šëó|]ù§À6ÃŽ)(Æ�Ð=ÀÌ�¾)S¦dsO�W ìÏržm˜·�Œ‚NªþÀ-gÙü@=g°ƒ@•£3YN/µ  ¯ƒ:( –ƒ À…;¶Èø…U{oõ5V�69ÏÜ\ã8çœsÂÕ¶ægîæb,ò¦²š6mZ¸ÚÞ3Ç“N:)Æœîa‡ípÀê>c°Ü\…Å´mLÆgÍ…Dƒ}áÞž…ùr�¹ØÖ�Û+üZ»b®¸>Í=+X/.•J¥R©T*•J¥þªgîܹ/4M³ê@¬$´En-XÚo¿ý˜¸¾àriå—r'9« PÊ«ª€T‚?Çe.®peŽp†K ­mnoB¨kåËryA<¸vmò‹åø ‚ÀÚ砀ʿØÇ|äƒy`ÈÉ6^¹· Þ¦¹£rœÓùVœ 8;2ˆœ±ëÈ%ùµÞãg‘0`ª¸hÙ@Ö:?Ø<å”SbM­�PnsÑ8–#m-¬5@6kOœc×iÏØ9ëÖÒY¿y60еðÀĸô9f̘(,&$šCÍIÐæ ZÍ�»À�3¨ëééYo¡K¥R©T*•J¥a¢�N;.êwèE)á×WàœöÞ{=öØ£Ùÿýã _Î, ÊÌíuÑ�wÞ&÷˜‹,ÜPN/h\Pí Iö³ð\ÀÈ�t XåÊ*¼ngÍšp¬Ð”#•@3÷™>€¥°a`®µ åg5éM6ݤYoÝõÂaÎðgù¾Y5`Ê[òÌá¿ T[Y\Ê÷îp®³~ô«@-7˜̵Õ6—X[@×X9Ñ�¸úž“+\Y;�ÕÚX{Pí^yÏà8§¯gmhë¤oÐîe#�$sÜè=;c²!Ðt¹ÿ­ �.•J¥R©T* wUt;P¸éÚ©�‘ž…s¹ºÀðŠ+®ˆ"U@ ¸ kæÜúž[¼û³É ²›yÀa@ˆAmVCi>²ÂuÝ—GC:²`ðÌ3ÏŒëäþº†+ìsЧß/Õ�ªÌB�}Æ‘}òÉ'£°À46ý ›.Í-U�Y豂T€”à �9�tœ�1[#áÙ®flÌ~Á½Ï¡ÍÊÓ6€+È7f€ÛØÁ5çHGÝšZNµÂa6¬•µ�¹Š`)ÌåÜáéÓ§´»Oø3¸ö,T{7n\<3ÏÂ|ŒÃ<šå�\p©T*•J¥R©Tp[Z¦à¾ê›ßüfzRð ˆq\ØÅ_ÐÆ‘fàOÈ-ðR•˜» >›°^R½È£O>òH³új«…ì(gÒ÷Ü`íg±)í�[×±æ*ƒ?…¤À«ðhnµP`ï�;aÙŠk ÑÎcR…:E0ÊÉ5?s¾ò™¯¾úê�ôc�=6ÎîÍ�j9À€_\Ûk¯½6·®6¹ÙÆ�€}oÍ�7rl}„F›;Тƨ]ëgÎî5_càêr›ÍÑûï|ç;ã8&9ÉrŽ<ðÀù¡Ïþ@噾6À¼�9Ó\ò�~ô£^nýÌÑs+•J¥R©T*•J¥¡¤ �n:»Bl… Xçâ‚0/ rË�‹y¼ÀšÎìå’ áõ>çˆr;…GsQ� 8ô™�] Çµ¤@�» Ý·Ã; ¹vçÕÑC`Ø}ŠW�>°ÇAÝãÇ���b!ÚB¢ÍA´÷€áæ›o.¬PgÀ td:Éæ¦:6ØUXÊÏÚØÖ…» ØU‰¡ÚÌiγ1ƒ^ã7^ë(l›ël�„Œ›‹1?öècÍ÷ÿíûQøJ>± ÔÚ�Ãy¶2ç8[BÑÍɦƒ¼_�¬mσ³lmŒÏ9ÉÞ×? ?üðÃã}¹ÊÆ:ЪèR©T*•J¥ÒpW…@·£`.æ5×\Pø€)xÝi§�¢¸ÒÉ'Ÿ�L…õÞzë­ú,Xµè¬ ^ÓÁåxM¹½€.�a òg¹²€pºŽ#ì¸×íµ×^á"�×€UÞ®{€.X”ƒ ú�®ëÓŽòk�£Ï„6Ë£ut“ã�@*ˆ6gP©¸–käßrr9Ò\U°kÆ£P-œƒVî²Í¡ØŠe™;·¬‚ië"d9û�‹kkÖ†l°ZÇ�7Þ8\iˆg¡èÜb9Ä\f�ØW=È7W㳞Þ36޹5Âup©T*•J¥R©4°*�nGÝÉÝ 0¼ë¯¿~�—Q¬‰'À‚-E©¸”œU®§{tsœLî0Ç(&d‚ZŽ&ˆs/ s�ö€š¢M\Xð’�0ÇWž+טc+·Uȳ�j vªðgáÅîåÀQÂ�]+„ÛÜ€£y o½Ú5v¡ÂÆå>�k l�˜¯þ�—k h…"·s�G�`¬m®4·AÀiUÀ‹s v¹èúëÖQën�[«,€e£»¼fΰMã}î¹çFa/ÏG°êלtã×ß„ ¢8Ç×fCÓ©.­-}6� �Þr‚ó½¾€rå:”J¥R©T*•J¥¶4 0àIˆ�,pq€…ƒÎ'h L>*àUÌ ¬¹[îcV$æžæY¿ ”CÌéŽÀS%e 2}¯/.,‡¬zO˜1‡ÖÑFÎÏåF«® H®’lœ\P�gì ÏK@\+¡ÀÖë®».æ A¼÷|þùçGÅfÅ¿„&#çU®0�6.×ëËZ¸Æ8¬#ˆ8Ù ÞzÚDHçÛ|4X稃PÀí{cåV+(æêiß»øËçµy�¨Í/�Nr¯ö…|;n ¸ËÁv›‹¬b´õ2óß¾zõ¿©~‚l9À¥R©T*•J¥a­r€ÛÑ€�0Sà—äürUG�p ��äSO=E�丂dŰ€XrôÀå˜iàl…)ƒ®ÌÅrîÊ€NH3¨ÊîåÌ O6&-œ¬�O'¨�^ |rDå+qÄáZ -vïYg�s·r 6ÍpË�õ¹|ZÐÎñÎ >�ƒÌ]—ÆiÌgŸ}vä(kü|.0Èó dY`›GéÏý6Œß]'œÚµ`4{_{é$Zð-dÛæƒ±[_.µM�sô6¬ØWì«‚^r‚mLš4)œz÷[KÏLÛ½©?îoó¿‘À¥R©T*•J¥a­àvÔ:�'è¨ö Ä„;ë 9{W8®k@°O\Nðëz°)¯ÔË‘CÂm*—×gŠ> —Þì3àØäýú…�sÞór½þåÕúŒ‚Mà þ.¸à‚€\nò‡?üá€WÎ-gS\b°'tsR�[_ò”Í‚Pn57¤M•ž9Ô€¼ Î3yA6(VXJßò‘A^ºÆÆjm€4ÇÕx¹°B¹Á>Øu­ñ¸xújô'Dð[Wëd>y<“kåQÛ,°A`CP[áÔÆ#ÇÙæDž#l�­×XÈ·yºN;ÜâÜ�°.Ö¤·ß‡þ†1�—J¥R©T*•†» €ÛQëç�ƒáÅ€K!&n&¨JŠ@9_ôÉimQ )o ‚O«|¦ ª `È•·\Iy©B”…O=¹¿`Y;®%î'xå¼*Dʼnu§yï½÷Žk° ¬]£/Ÿq{ó8$¡Ê·Ýv[§2N9·îB�6�ß l�S_�™ hA6Ûû®° õ’K. ‡¨]ޱPcn-§PZ0j¾™ëln\Wó pm �m°jÍ]'§ÙÏúÈ雘åŒkS™¬m›ÆnÓBñ,ãàÀ^»ž�uSàkÇwŒuv�~€òâ û¢:¸T*•J¥R©TªÚ8mi@` Å%UÀŠ“è%Ä€*¦p�%Ç”C Úüìh ù¥`Ü©v ¹Ž§œrJ€êرcçŸYËÅÌs9¼Bª9²`—ûë3ms~µBÁ´�ß3Î8#ŠÁ%PÞ~‡íãl[@éL^®pŽ DR¡×Bˆ9­�øz…¢€­ùf86(W,KßÂ¥ÍÇýÆ®³¼_ÀnÖ|ruAµ¾ŒË½ÆÂ·!�´²²ž¾-ìÄêË}u?˜Ï¾……Û00O |ýõׇ‹k¬ÜaùÌÎ1>ä�Cb-m<Øh�ü^6�|ÀÖ\¾2€öžõ×®O¸í?œå�—J¥R©T*•†»ÊnG­pV~EBdgjÁÐSØÉçŽZg�ušÙ³g7ç�w^óÈ#�„SʽuTçIá¹ SqK…$Dù­€ ð¹†ûÊ]N79�s@s3ÁWÛ` šå%ÿúW¿°,·Ø5ÇsL€9·7s‡å5ƒUׂn¹½òaÍWUe€È¿ñÆÃÍ…—�xºß<ŒÇ:�ccÍ0e�ÏÙö= ÎÌ]UhK´ªÔà›ë«]› Æ¥kÂQ6.k äÙgBÈåL·Íëá™x66 Töæ¾rz�K60&Ï8~c²f6=ôÐh?Ÿ}æ7·­àR©T*•J¥ÒpWp;j€óxN£WàÈY¼á†¹sÀT‚<ï+¼h�k«*3W€�.ð и¾Àè ÛrÜMÐ+¿|ù¤ºGž­±€APƶBt…� a–[ ÊA£¾€h5jY™4sx50äôy¡ÜÜQ@ b9Áæåì\ã½Úörœ?ùÉO6ãÇ��vÀrº¼�Õº¿¾…‚“¹ u6Vc­Æ£j³÷¬³6}åVë{òäÉÃÖÔº'Ï8¶n�— ùÕýæÈß~-œhkO…iÀË�wî¯ðhëø©O}*B²�Ñø…Rê”yqõÛVp©T*•J¥Ri¸«�¸µÀö 6¯½öÚpH¹–�dÊsUõx£�6ŠðY!Ò\X€wÔQG…[é,Z°\9�œFa¹` œ‚R Ë-Ž Lor'–ó :µ –…!ƒE!º Ä—Ü\°&„˜«é}î*hz PjƒÃ Ü#Yß yÉ›½ÂšÍQ_B·&pÍðjm £¾ì²ËÂõæþúLˆ5èå»0ƒ[?Û8�—ÚVÉ9óy�² nÎ Ôú¹7stªþ¸Û Ù¹^´pi}ª¼­Ï·½õmá"ƒs.¼ïÍѦî»î}rt3ÄÜ�I[Æh l6€uÏ d{¥+Ü– €K¥R©T*•JÃ]Àí¨u�‡B€�£Âš…8çñAàV¯‚JY?ï²Ë.ÇXù«œHÇ&9žôf*0NU$Ûë­·^@WVwæÆg fAhVD¹Úh\Pè^Ž&ÐÂúáÏ™3' AKcÊ Ô@Yߟýìg#”<Ê/VÕZHwB©¹¸O;@rÛm· p5~cÚͽå['޶±úÈLGéœrsÍÓ†�¨ÓB®µa�Ú�_Á-€ì{÷›§ÜZúYæ �=;à«/ªµo�<?�oÌž‘÷8잉|eíXgcÑ–õ¶žÀ¹¿ÊpúÎïUp©T*•J¥RiX«�¸µÀ RÈ,€SMXÞ¯Ð[ù¤ IEhN(ÐÊÂ�ùsæ™gH áådNœ81€Vµa0ÄÕY-d�KPKYyx‚p!Ã$÷Øo÷€B§>…ü@?«t >9¾®Ä€Tóž{îýrãÐ'‡”jÐ'�X_`W%e×sµ�è”)S"Ú=ÖåÊ+¯ŒñZ'!êNo¿ýö: *ÙòØgæ/§¹�h¡Ë dÙ40!äÜiáÖ`{ƒ 6ˆu²vÜ[ž�0f J X¸3ˆÍ#Œ„_`po|ú‘§ÌÕ7_ÏÓÆ…q™ �w¯¶¹Á6(¼ú*ãõûâ«gVç�—J¥R©T*•JÀm©U�ÎsiA"8ãž‚:PL½/ìzÏq:K ¥ˆ”|Zá¾®áTªR öN=õÔ€6߃#ùÄ K(±>åÎrœ�3§™3 Èjâ‡0]Îì–[n -ìx‚@cçžÊ f¬—9ž Üœ�¤ë�­Ï6ÇWú. ®¹ºRsï7ÝtS¸ª€°+°¥º5 W š£Ìõ¶Fæ9sæÌpË…u»Ô‚qýÚT06À®¹æš€Spí«ñçÙÊÆ­_}—µôYæAgø´ptkeÞÞKçZ±M ëdnnëÍ…Ö¦jÓÀ¨{.6 „Šgx_‡Ì׆‚±å‘JÀ¥R©T*•J¥á®àvÔ �àÆÍä袬€,„6 Kq~3ä7Ý[ðxA§U€Ðû�õÄOŒpe�æ~`yòÉ'‡K l�s:Ãò{Ý+\—“ Èä²x”»*$°*d°Žò™\bP�UŒA$wTî®>¹¬@ø‹ïA«\^sü\kaÃ@€ ©~ôÑGºA�ë8¸ò¡¹9jK5l� X�ß|­)D�ÑñM6�Ì×8¹Îæm〠˜Á7(M�j®]n­°k0¯x¸w}:èÚµv�­ ÑÜqp òCe=9ÈéÌç¹ÂYÉÚØµÓº¼ÌòümPtß_�\*•J¥R©Tî*�nG}·ë:*†#ˆ‚\Oa²rÁð7Ð Ì8¨PÅ v�k½� ¯£G�×S'p D¹™ÚÙ³fÍj®ºêª(Ê`Ëâj ÆÌÙåZ‚g�°¸š®Ýn»íº9ªÆö8˜B��§p` …%»4Iã�“Ú›îŸÜQP©áÑ œÚpøæ`«º¬×öœsΉó„}n=¦¯ [XµÂWÖX�,aÍ ×ÏÖT´Ðj}�Q@n}�µlÂ’A¾�Ð+ŸW¯Ÿ…7ƒq®;à¶FœeÅÊ�-˜ùúðU;6%8Áò“Óò\¸üú6&ëÛŸs€­[F”J¥R©T*•J¥R›ê7�g®&`U8ŠË)LØ™·ÀêòË/�£t¸”� 4Yऄ ƒaïsþyúT>ÿüóà —\X@)”XÛà HQ¹¯*NëW(. j rµÃ}Ìy\P““ ³ÆœaÀǡն¼X”e€˜aÒB·Íô CÖ6}8Î(GZÈ1pÔ/°5fíƒtsû@Ôgηûì³O´©έq _¶¶\c š `w�=ö˜Fn3Àø¬ƒ]"ó–§ËAö<ŒO{Æ®}0œy·Öä„Nˆ� m_Ÿ¯qi´ŸqÆá [Wã®] MW>x_ŸRÙNÚ+•J¥R©T*•J¥T¿C N€wø ÙU¸iÿý÷oŽ9昀Ƌ.º(œLàÇi%p¨Ø’pZ ű…Çw\¼¸¥Â‰9±ŽH:âˆ#vÒ^, 0qB…ï‚H!¿Y�Èev\\0Êmy ì@ÛÙ½ÜN�â8É Ð@Ž)°æƒM`œ¯kô Ðü»Þý®8ÉÜ�Ѽ…»ÖûÆ`…bƒG}‚|�Õ¡ÐrPºÖGq/aâ@Yõk€o¬`=Ï4æäêËÆ‚ö8Ûúpֲ뭱McâÂzß?Þ­ƒÍm˜¯õµ9¡øVåùZs7/÷‚cýû¹ ÷V6*´ý /<»Ê*«Tt©T*•J¥RiتB ÛQ¿`—Bg³"±#r&L˜9¤`Kø, Ìñt½ï0YIáµ*&«’Ì•§ \q;…BaÂB­ÝËYÍÜUŠÝo`\s$…æz_. w °É{å¸;aÑÜ`àè>ðÍ9¢ìs}*²e,��ë*�×Gûì½OäÏmn±ëܪz;0… ûò ÝY½ú4w÷¥;mŒ®5Gî·ù‚xU¤Ý'L¬ZCsµnr‘�ªÊÒÖ�³ v3Ù˜¬Q†œ ï¶a�jmb�jm{qÍOH¸ûl*ölŽÓ¦M‹µð¾çÞ†lªXçtÁ­Y©T*•J¥R©T*õWýr€¹tœD�ÆQS[ ÄudŽ¢PZáÍ` ‚aŽfº� oÀ H�1Ž)Ùç7ß|ssøá‡ÌqC}ÎEš@ɽ€QÎ,çTH.�°Â®�* vF¯>��0c¬ã‚.¸à‚€H`'Ô×¼ÌG[\W¡Ç@ ¸+ž¥oók:E›;ì°P´À ´+ H¹ÞB™ÚÒ†ùš‡Ðjëâ3ó•¼A¼6„3sˆ¹¯À@ƒó+®¸" ÐpÀáðê/ø�ßú ÓÖ?àÍ àÛ|°fÀ×&çVÁ/.;0½@Ùçž ÚZsy¸ç침Ôml(¶´ZÚ�fÏε«®ºê³MÓ”\*•J¥R©T¶*¸õ€³P¸*Âd€€OŠR�£¬¬PÈfÜX0 2�,—  YϾJï ™€QŸ@òw¿ÿ]yÒ—"SLàÈ…ƒF…«€±>õü@£þ�¨þ‰sÍ­6À ¢µå|`àJ%VÿÎÁ¼æÁÝ‘ š£Ì]6WãûÖàr‰…‡ƒQE¤¬î~�nó€Ã-÷XŸÖCx¹õÊÖÊÜ�0€ç˜[ýº$›·õ²ñ�Ç?iËZØõÖ[/Ʀ˜˜ö@¬� ë¡×[/íjKÖ@ÀØ3ÉÖÎs°6–ö÷Æó Å�,ô¹ëH¥ª]*•J¥R©TÖ*�nG}�¿`üÀGy$*/ƒ"på ‰Æ,l4*®"ä€rtµÁEƒ�L>ªªÏ¾çüØ,�J O?|ö‡á*{Ï9¹À HaË 4êÈ‚5!ÃÜb/�Í15‡tŽ9›@[›� ËY–ËlœæZ�Ë}/”ÙûQ°n¢±6Ì?sœõÁA5Þ<ÃWˆ±0empkå&ƒM@mÌÆd­ŒÇ:�R@¼êËW 6F?[§ ±éæ$oüjÇ3�g ÈåWkϵîÏ¢\~Î#­<c€o#€Clͬ/—Z1¬,®Åõw¿>–äîjÓ³[šó‚«øU©T*•J¥R©TjSýÊ(@Vá$`§ð°QùY!(9ÁÎ�ywß}w8ÁBŽª0`G%%q#ÁªjÆtíµ×6»ì²Ë|`~ÜH°Íé[[y¨Â¬�Ï`ùÎ8œàSž/g–ó >óˆ#Ž'� ,Ô˜‹J ‹ ú¾ûî‹ÐhÎ1·Úâš‚ecÐ7çX®0ø\{íµcn€dëO{à”jø[ù¼ÖÑZ)¸œ3ŒØFãæNs„A©6Þûž÷F¾0¨£®÷=ð7}¹˜[ƒ ­öRÅYy,•÷lB€Yk ²óX&m阫ô�βo¹×Öè›÷Òȳ±6uÔQ©T*•J¥R©TZžêw,Ð(<¼.9«*Ož<¹9餓šG}4ÂcÁà¶`•ÓÈñä.Úx ` ÈÍž=;àŠ `‘–«}¹œ¹w¯½öŠëT0v÷U? K�6+6 5æÂæy½àc�=6®¶€X? ËQð:�üÈG>€ZA!÷Lª¼¬o ®²2XžŽzâ„sqµÁYõ¹69ªÖ‚{jÁK‘0ÇOð `ª¹lëbS@^®9xO`ÒF�è·ÀÜ=ú¦æ‚µgÌÞîY´Ìxm.§õó¹ùë ètÐm~Üokçfn½9ZC}.� |K¥R©T*•J¥ÒŠP¿t^n¯�[¹±œD!Ç  ü‚[�ç3n§Ð�� �IDAT� ¢8Ÿòi…í‚h?»Ø‘Ï�ð‹  q()‹KqlÁšûóX¤YP<uêÔ�O޳ŠÓÚ &¸ã`‚W.³q‚D@Èæ˜jOòvåŸ|òÉQ, ¼Ë œŠPc.¶0o…¼\kìÜ[ÀkΚ9Ôaî¯ðp rqm…†JÐ.Ü}æå>²¶A®{Ë*Ö®Ó_†¤kŸs �3ŒœÊK6VŸY?óâJÛtp�ù«íùÉñµV®Ñ`œ Ëö ¹÷6!n½õÖh?û.¸-•J¥R©T*•JƒU}à+  `HGRV�T›3n£ÏÓ¥‰rzÁ¦œX…š€ª�\¡Ì@ 4ºµ Ýš VAXÖ§{„ËËU%ÐÊM€`pöÑ£G7'œpBsÔQGˆ:~è ƒ G|r¯µ¥Õ™ï½÷ÞpdÍiÖ¬Y‡€š0¤^Æ .]ËqïŠV=üðñœß-¶Ø"ò¤Ï>ûì€~N6ÀëÎöµž ûÁŒ9ëà Л›õ1o®6çY?œÚ¬Ä-ŒšŒ“ƒ LÍß³°à�Ï=¦<†Šãn¼ÆáÜe×s…­™¯6ä�ƒnŽ?p÷\�¥T*•J¥R©T*•³úUtq_� x’¿+OìqLå¹/¡Ã@ ƒ+_Ý|å P¾çxr$�(ŒB–I˜­—÷2Ô8U0Ç•ÏëÌ`÷�gŰ�5èäj ãUˆËuÆÏ9õÕ˜9¦`øàƒŽc—›l¹Ûàè;*IŸÚV͵£·ß~{@²þ@4pv-€߯*÷˜l~ ÙÚY..È5w®t®²¦œl°É±Öæé§ŸóÑ(µŽÂ˜ó˜&•®¹Ñ@ߦ��c°¶6¬½u& –õ§€hÖ§kÍËØ´Ÿý]wÝplCÂK^´9pÙÍÙÆ†g;êéé©*Ð¥R©T*•J¥a­ªÝŽúu0÷Œ’üU� ¬äÐÊÕ•{Ë™vëL[%W“Û Á xUah Ñõž|TÎ. Æ@Ùõ@™ ëÐ põ -�åJ‚½ýöÛ/>à ™{鮳þˆf~1àNW�ûª}° œ½€ãرcì¹´`tkhUE¬�!Xÿ “+ì^aÕskæ<aÀ*�ÙÙ¼æjŒ`Üx ó=Gìr±'Nœp+üØ=ÖÊ­±¶@èºë® ªЭ¯¬îìÙ#Ø^mõÕ›gðƒpݳz³� ëÀ5ý\`ï�æI“&Åsô\=KPonéþºßBÀ¥R©T*•J¥á®àvÔ/�n:N0ÐEÛl³M„×rCåþr¹“*({_èë®».Âv¯"JÀV.ã,J®,`º l�[ ²€HÖ§bPœI!ÓÀÖ½à੦ì +wU;®ÕÚOØÃÚw,�{¹ÆyT•‡ì�Ëýä+Š%lljÓ9­îÂÎ=Ö¯v!‡ÔxTÅîV!Öò¡A&�´Þ×ø5 ˆÉk–÷+<[ް1š‡~óœ`kgÜ6l:¸ß8Á±�eë®Æ�õŸÕª9Ð�Üü¸ºÚ–m~™',OÙü<ã±)á¹p‰mXØ Y0 :�Êê¯ €K¥R©T*•JÃ]Àí¨�æœÞvÛm[Bu-÷ÐûÜE`È8–àŒ@X:Ä®Þ ^ÝÃ�ä\r‹9¯®¥\MPÇ…ÌBZ^`¼eåc0¸ãÞop þÀ`TôJ›Úð>¸â,¬ùÐC�q€ØìK{\_E¢ŒÈ o·ÆÉ­õ9 ï>wŸ¾�Ï|@¢pg¹½à_-Ùý`[_>·a Û\¬KÙ:úè£cn�ÐsÃÝ ð�& ·Î68½œ\Ï#7ÌSHµy�e!ÏÚp¯M/P ŒÍÅó�ÂÆ €]knnŽ»¾…c«móÁ¸¬ç‚¿m¨�¸T*•J¥R©4ÜU�ÜŽúÀ�Gx¬üPÕ‹�0”\Q€°¸Á@ ÄrTåÈCßs4… »ÇçŸþô§NA𝠒óëås ²A/ä`f±*p§�¬^Ìyå g榺—; „�!]L¡ÆÜkE³2„˜š¡Õ \ Ý6G°â^Šh¹_;Üb€xÁÄZpN�·öŒì‚o )ŒìFî¹�f÷jÇš'ðäøúšm ×›`ߘ¿”E½ò"°jÌÚ¶Ü]P¬¹½@[cá8çÆ�¶¬™± !çêƒs@ �å™j—«¬-�œÎužéܶ €K¥R©T*•JÃ]Àí¨_E°Hµd�)ÄT …uF.àRù™É:›y® ”�Uaµœ^д\ò¸® Ü‚j �9�Bˆ�¯k´‘° N½/DÔɹnÀ4sW7Ö÷ÀÑg`Ñ÷ÚÊ,ߨ�jà J�¹�à]vÙ%ÚºóÎ;¹¾ äo/Pìs.°ö}®È@ä"›“>½çX¥]wÝ5@]ª7ƒæ 7Ü0*4Wº¶Ú¸æe�¹êÖÏf v?�u­1X®®Mýe¡1cæ�çqHò™m�`p¬u:ùÖÏ3̳‚Á±uò|å˹í¹´åøöò»V�\*•J¥R©TÖ*�nG=sçÎ}¡išU—µ5°“a³ª<D°&„0�EPPA—^ðËa‚ X+€âR uvl·X¸.‡Q¥a 'ÜX§9ÂîbíΣ~|Õ~†ûªm!ËÀÑçéƒZï«Xmì ‘Ëên¶ö…C E÷ßû×ï5óšy�Ž@:ä�CæC³°lŽ2Õº¢BŒ}F�0ˆ”®Q!èT.¬Z™Ÿl=Í‘«ªëç{ãÈÂ_Úõ³9�_m)¦e­\d½À|ž XµÇ±7n°þmpzA,·ör‰�׺X[ãÛò’Ýo­ä&ûC(\\Ûž¥¾Ú>¸“GüXOOÏz }X*•J¥R©T*  ”Ù4ÜÔg˜pF�¼Q®¢œ[aÉ€dÉ!u ¥`DrDÁ©üT•еèÀ¸: ì6Ùd“�T€ÉÙ¦òXóü]0}&¼Øu€Óý@ˆ8À 9« —Ó ^AªÊÑ < Y¦ÍiÞc�=æ‰âˆL0i^ vûí·�~Á£ö´e¬æpõÕWGήq�]0©âs®Él}Ù(HÐ4V°Ÿ•£­“yN™2%®…[—öõɉu�qÙ0à|›º¸ÖÃúèÓ¸¹Ðž žK È}n~À×:pyМt¸ô˜ŸÿÅóáš{6Æe“ìƒqsïžK†`„Ê.•J¥R©T* w•ÜŽVîk+ É q<9«òTA7×ÏœC�Žå³‚§=÷Ü3 Ôµ ÌWÀêZ ÆÀ�<W“B~ÁÖ{Þóž€_pÌýtÀO%hßëèo÷ƒs?ƒBÀ­_@þ�¬Pê½öÚ+`Uþ-È{Ƭ=aÌ`Y˜1÷™+,4ÙxfΜmé“+ N�?þøf„ ó÷¹Ü�ŸÛ*,ܨÐfs—Ã+ï˜{j¬ÀT;ÆÉqåê Söì½<Ÿ×µÂÈÍ”_ã> ZÓñ¶ùà~�y®°µ³ÎÞ×&ÚÑNS§N�õænO›6-ÎRæ°+næzsÒט1c"?xòäÉ‘'-$Ú¼šÎ¦H©T*•J¥R©T* Võ+üG`’À¨pÛ£Ž:*�,á d)>Å9® Íyî.ÇL¹ /¼0ÚâÌ‚0ùÁ`lq29žÀÒõ hkÇxòü[� Z�"‡•cil\Ê<ÆçÁl¶Øb‹ÈÃu?÷ÓØå¿ªÐLÜ×Í7ß<îŽÆ®Á±Pe/¼äÄ —66Ÿ¹Öü.¹ä’�\9¿úñWšc Á¬plޏœ]N®õT$ «Íƒ”uÑgÎßZš³ù{×dCÁûYäÊ{®ÍgtÓM7Å<� ØëÃñM …YaêæÆ�SX´ö¸¾ú°Ž6�»uÜqÇã9 WôÀªBiJ¥!§q xâ2L`NÓ4_îúyÒBW”J¥R©ô"WŸ�ØšÁ'€ˆCð8ƒŠH͘1#ò‚½¶Ûn»3vl˜â1 9¾ÜFÕ‹Á40©Þvúãl Y\Ç ûåvjû”ùšG �;ðÌýÔŽë8 SµfãÖ¦v8® xÀhîªy�e÷:¿@‚f!ÛÞ¦lM„Qs}¯½öÚp�¾ªÑÖD¸°ùs•�ì*BõüÏÞüÕškÆ}Æâ•ç çqOÖ Èzß=úÊã–\˜…€g¸3÷— b­…M�îµy>òÈ#óÏQùyF°0hóÙ�X{ ×œ8ãBÜ9Äúæºk,ë‹«=p�¸§§g£¦iZ胡¥gæÍ›·ö2̹­‡Pd<¼4½iš1-Ìøá¦i>²Ð»¥þðŽhéù,J3ü]S@\*½hôbø¿À¨Îß{õoJ©uõ9tqx½€P“œÆãŽ;.�–cÊ¡åÊÊ}qà-ßNÂs¯¸âŠùplóü`mEï!æ@Xäò6aÓJígíY°(gWÛŽ4r�¶µ#ϤDáÄÆïZÀmÜÔ…)ƒa.µ¹ —þЇ>!ÅŠFqS¹¡ ÙÂò�åé‚g!ÉÚ"l.^àW›B¯å�?ùä“1î—¯¶ZômÀúÈó対Æêóç xÁ6åqIés•9ÞÖ+ÏäUÄ ôXãàyŸ¹O¨ºû€°¹qœ3‡›„aÛ(0f¹ÐÞßwß}ccÂø´c Ýì¦ÅR©TjCuþã7j9­fÂõÄŽK<£³™Q*•JË[£:#kåK¥>Ü x8�Üߦc íæ›o�ät|ðÁTœRðÄ�ä‚4À ºÀXš~VYX®¬ïA¸d®ƒà×g`p�^Pf,úº€Vn,çD«   b!Ë>Wä  ‚Ví›{¸Ð§œrJÀ3˜‘€ä 5Òì3ýšÿÇ?þñæðÃ@Ô®†Â¶µ ^3ŒZ8øìÙ³¢¹Þ®áÄ—Ÿï¾ûî�N÷3Gúõ¯{}|Ï¥÷¹ÚùæØíöf˜´õñžÍŸëÓ÷Æ` �ÝüÀ¹ÏÜ#[™mTp¯mXïËö™µÔטËm}<Ë‚ßR©Tê—€ï�È•å¿ jT§ÿ':ã)•J¥åõw�¿wn)ø- ´úÀMW®§\TP*Á÷T.i†Å‚Uî¢ðÝ<0€° £¡ŠcÉ!¿ �Ãé\Zá¸ÎÜZ®U Ðq1ó|` F�Áç® ^•—¹Î\é[o½5À� ëUqù†nˆnî®ñ«ö¸°àR~+`|ŽGÊPlsäÞïÞ{ï�ν�Æ�ÅÏ=ôPŒÓüÝ+´ØçryÎÆ€U‘– l�|_cã6§Ûm\À؆ƒöÁk'L8ÖF´Í�ë¨Á2÷Ûó°ÀíÞj«­â{œ`ǹÿ˜cŽ 0Ö‡¶ÌÅ�É�²võgMä*ÛÈß…t�K¥R©´LZ£óŸ¾‡ÑüFvÆ3}¡OJ¥R©=ø––»úì�&ÀûÜOŸ #)„—{ÈÉp`@=ðÀá$ ÷•7 ¼À@ÌœI0 z9½ª)kÀ�A€ÍEæšæ·äh#Åž�'XR,Ì÷â‹/¨æÚ‚9ð îÖZk­p[…õ‚ha¾@\uf!ÁÆ W4ëlAïën³Í617ŲvÛm·(€e<Æg¾š7Лòƒ… eðn¬Æ¨n8°4/¶ª×Š ?çœs‘¶¾œ]Ž«<h9ÍÖWûÆä«¶­ HèûØÇ"lÛ\r³A_Æg�¸ÌÆŒ­ÓøñãÃÉ5V®0·×u�÷3ŸùL<›6l*È6÷{¦”E³Ê .•J¥¥ÖÈÎþV”ã»$�éŒoÄ®+•J¥eý»å»¾¥¡•Ó½[hÉkÝûª?U@hÍüYPÆá¯\Í 'æ"0±Î:묀eï/aЊJ�;aÔà ¤iãºë® ¨±œátZý¬=€í}_� ¸æÐ§tç�w@u.¯‚Rr”A#Un(å�k�c $…G“q�eí»†“ì¥Ò3‰Æ VÁ£1�Oé{cá kÇ5\X .dˆs‘Á·÷€8GØ‘JæÎ‘ÖŽ �ë¬�XoÂ4À·Ž@Öº÷œCž³l=¿²Ž`ØP ù¤“NŠPpí› Ù�ÍkªOïy6ûï¿<O\i@í™ëGŸ6ê(¤R©TZj •B}é¤S,«T*•úª1]ÅýJ¥¢>Á:ÂsS )EÂoU„XàŒÃÉ�^ÀÎ÷BŸÁ-�º,?˜Ó 0Ú Î¸‘`‘Û*§Xeiî,èË*Ð`„‘�acÂË™}Æ�ì´×tKïI9Çò]9»`V5 äòrd¹œ@—[�À lO>ùäpkÍÛ&�x4cî¬h”su…‰»Ÿ; vµ¡€”k¹©Æ%$Ì ÍæÄZ£�7Þ8@ˆº×:›£Ÿ�ßWm�~ëª_@ëÐo½³u37�m<î“»lí…œëˆkÇór¿ùg!ÛÆàYȉ¶¦6,|¯Okçï[ó‚àR©TZ¢F±*õ#:ã}_Ó4Ï/ôi©T*-^¾¥A£•ú®š Ê€œ|Y9©ÜYE°Î;ï¼æŒ3Îp^ª=ƒ* )„|�fn©q¸x æ’l9ÄéLrB9¢Âp¹™àŽ«Ì�Ÿ®qØrÆ%ÜZ8óÌ™3£h–0b`ÍéÚ{Ûm·8;§8sd¹»ásÏ=wþÙ· ™ûË¡5vóUÍ|r‚Ý«�Ç,*&ï7Ý] Íñ;vls饗ίà ô¹áæ ¤Á¹u4/`zùå—ÇëߨÔ?§×úyϼ9Ôr¬ÁjVhZ-dš+oL>“ïk`ئGÜñT ß„¤st}µ& Üx=0žg;“>¸ñB¯Áo©T*•–J#†èm#*'¸T*-£ÆuB�§ü–‹Â íoÎ&�ªy./71Y¹¼€Ì÷àM, A–°_N&�^ë3pÆ ¢`Ö½®ó3hÕ6°”`Lÿ@dî´ÓNu®hìÖYg��q  æ�îE]Ÿ+ÅÑõhyîQD ä<}º¸Êëå|‚aŸsW9¯y>¯Ü[ë¡=cµÀ�cÍá¶ÞúWAšËEÁ ÕØ€»v³²tº¼~æ*Û0ðžµõú1÷r ý Ú¼õp¯"[ÆÊ}ŠÍ�³}ÀÄzníp¿UôÈŽzÒ¯ë9Éœ_€?«mû—ó[*•JK¥é�ÂWCQy6çŒzÔ¥Ri1×y Õ¿ëJ/bõ¹ t \;Й…˜À"Ðã$‚K®(PºòÊ+Ãx*Ϙ1#>çh‚?.1¨ˆœOù´rKÇŒ®±Ïwß}÷fêÔ©áf‚áÑ£GœXà:nܸ栃 ¦@-ÝVN²1‚Q¡ÌÆ è–0ìŸ<÷“h/Ý^p ¹Ìï{ßûÂýäðrZµá%Ä¿Più°œ[!Óú2Fª¼_ír¢­Ã©§žð˜«@ì¨Q£¢}9ÉB¼m�|} '·V¤]àj Ü{â‰'Æ�‡c«-…´ÌÑýÖ…ëmœ6'¸Ë'N P·®ÉñÉy¾à‚ |UyæV_rÉ%±~�߯À¶1�–=sM§tp©T*-Qc^Ç ÕjK¥Ò’4±þž( V­ÔßœÍ,ÎÌ€RºÉœTá¹GàÅiÜo¿ýÂõä ÷³`OìöÛo ìz@'¿,rJ¿öµ¯EØîÕW_×*f¥?ýr/¹µ§�vZÌ�+"øÀžh€åÆ“0ì<¦HqªvØ¡ùú?=ŠoyûÔ| k\Ühí‚@°Ì¹V9ór0xæ«FÍE5> æ¹±ªaË[¶IÀñ°Î;ÒÌ¥¶à3îµ¼åìÈj<ƒÜ�6Ú(Üa¹Ç ÛW!Мôc�=6¾ZSήµ²Î6€2GÞ³²¾Ú'Ðì}ã´ù�Ç*qwmNX?› žƒMùÛæœÇ$ye5ðR©T*-RãõÁ҈ȗJ¥R©4äÔ3wîÜš¦Yµ¯JàPØ­âU€8sTéÞø†æá‡X¦� rXÁÔ)§œïƒHέ{äÌr9Á"夂S (ÇÔBÀÉ…ä6gõiiL �kC®ŸœâYÜ\áÏ � â|3¬9á.ónAdÛÄåÈ*ZežÀÝ{'œpBü<gΜ¸,ƒt�Éí墂uãÏ�h@Ëå÷ªUg88Ç›+ÌÕv±ÊÙÓ§O� Ÿ9~É&§ø°Ã‹¼hάª×ÜYã·q`]À±õåRkGž³µ±N6„œƒqà¬ÿ-¶Ø" Ђý<ëWèµÐnnºMóó¾ùv}·íwŽUz¬§§g½…>\‚zzz†J…ÕÅé™yóæ­½˜Ïÿ�zzzÚz�u–ÕðÒô–€æáN¥àRïÕ9ò£-)F5©³î_^ Í�þ6 ÇY1ˆ¥þ»©T*-W †ÿ ÔÿGJƒV=øÃ€û“ÌÝŠS.)ÈOà,+s/A¦óyA'g”k H9šçŸ~¸”TŽ-Hz@ O™2%ÎÅÕ°»Žñ" &ÁpÎ š¸� Zˆ/�æ–r8$È©ÂÌi �®v„ú^ Ρզ9Ê5.ýruÁy:§ò‚…§+ÌWZH³µå¬‚T°)/ÚØ.»ì²€e°Á¸q_~ŸE¦ŒËqÏmXO°m< ˜K+|Z•hgT.¹pmùÈŠuq¹Á½�EòMn´66ß|óGwÿ5×\k˜=Oà+×ZÁ0c·Îž­5Ò)ãõ;À]¶°¸¼`k²¨ÏºU�\�\Z.*�^>º¥Åó~ç4M³ãBïö®�:áˆmŸµù¾^À»T*•šàÒ`Öüs€+‹àTy?xåØr�¹½à �£÷¿ÿýQ…˜#zã�74»öÎ;ï æž6è•\b° F9ž¤ ·Ö×õ×_?€•»Ê…–» °@2-ôXá'pž…o¶Ùf‘ ž�r¯�­Ðì£�>:�ЂKcš@Ö@¦±�È<¾È\%—š˹©ÞWµÚ\­ÅøñããL]ùµ6�8¶Z,TÇÙZeEk›àôXk�ž…Jso¹¹ækœÂ˜¹ÎÎï5~…­9ä�€s ,lZ›6�<?ïÉçÊž‡|k®ô‘GÙ<þøãá›§MÏä’ó†fó÷'‹�-î÷Éï†ß™þ`+•J¥!¤¶œØe�ߦkcâ¡–!xTp©T*•†šú� b¼8·y~,��M§hÐ䜂-!¹�•£ÈͪîR™…SgÕgðU|æfæ‘JBn¸:¬ð·×Q=\Q@¤…ý‚aN±ï�­«@�r\\E©ŒÇYÅÆ”¹µÂ~�I¨0H… ‘ƒ F�ô^{í0�Y6ÐL'–ÃÍý�àÖúp…�²¹(fìÆ ’Aº ¯Œ›+ Ô1HísaÎ>óÌ€3'YûÜs›�À¼'xWlöì٬Ʀ2n´qm“ðn`Íi6_ðë{¡Úò§·ÝvÛp§­‰õòÒG>û¦ãìà%�mÍ*•JÃL#[,3i¡w–¬ç;Üæ¾C½˜W©T*•†¡VÎÂEý�‘Ì™]Bf½`3Ç5N«BX@X©¾Á1w’³)´V˜4Vø±S° 4� gõˆ#ŽhÎ<óÌ€àÌIåj& 7÷ ßfB€Àäˆèt4¹ Î+æ í—®‘l|a€ ,…&ƒhm¥Ã©è”v�»kr^ƒ]'çØØÎ>ûì€G æ9×ò�¹äœnÀ Ú9Ëœ[ÐÎiU K¿€ÖñGÖÙ¦€u°®ú6>áËàõÞ{ï ']·³}½ï¹xÜh`n@_[€Zˆ8€·Î`Ynõºë®÷Y›É“'�sÙÝÜõã~ÏÊ\\»àïCó¿áË ý¾4—xðŒyóæ�]èÝR©TZ²Út^û꺂ßã[<Ë7¡¾M¨.•J¥Ri@µò¢�ei•÷ƒ;n œÝ5…•Tev-¸Á�SÞ+Ç—k«�8 ¬�2WØõ\bí “ Ž; nå¸\®°~…íoÀh�#84n²k}îÌ]ЬÇ“ñm¹å–ñ�\P04…,sy fp pÁŸ>�/øäBsb³À7Ø?õÔSÍ´iÓ"—Xåhs1ëÂÙÍi0 ’…b ë–� ŒI$™S hÍÍ|­±6õ{ÔQGźeQ*aÞÜnùÉ •S+?™ËË™÷ùÌ™3c}¹ã¾¾ãïˆõ2fkvÌ1Çxs²=m›?k,}ÏÈzX_@¼ Ø. „{{¯4l´Æ9§})Ô3gx~žM:r�y�é'ZZÍèJõÅù+ µyH sNËÇ“ŒXŽaÐ þ½1q¡+z×ñ]ï~¹>4b�œñ¥ýûã™þ.œ³Àß#¥Á¯Q�gÝÛïxþ»PϵTê£ú]+ó?�š<YÀˆ8´MŸ�7N#(b|ÞyçP]ù¶ 4ºÎW`ÅÑU±XÞªk9žÂ¡…sxåÁÊyU˜ Ô2.'—™ã¼õÖ[GÿàαG i9ƒØx²È™{›•�9È€Õ½Þ×¾û8ÕÆª˜–Pn („˜S ��¦Þ—�� �IDATG›æï:.«kåèr¸Í (\c–œ_E°„9«â¬_Ž3� Ø�®õ±r ¹Ù�™#îgíƒn!ÜÖ•£-o×XÌ „{ž� Ž3€Ö޹)VÆY6Àm��”»�[mîªS§|cPnl ݺ'ðk·ûŸZü.­Y¬Aï�â"Xkt�º<Â&'õòŸÀå¥qðm«ØQo:~•ûªY«íÊÈ©I €ÏŠÖ¸Eüg¶/:¾Ÿ›!mã;ÀÎFuþ<µ}|TþýÐæŸ£64n��™ÊMÃàÚ?ÔÒßík÷êÚú·~Fç÷yi4¦…HŠÞþìlÔiwi7Jó÷xÁ¿–×ÿÚü{­?ê®JßÖ¿gM§Öœ…Þ]vµµN¯¨ˆ›vÔÊÁ­B…Á £¿ägÇù€+Ž"Ž�K‘&y´JÐÈâì{î¨eN-hãÖ‚Üw¿ûÝ|À„rzå8ù·`L_³fÍ Ðp`’û 8µ>9¸úç¸WN¯°i�*·<ËO¶�×|„Ë}ð®Ñ�{�Ø›6ÌÍù¸€Q.}ƒMn30·œm@k 6äØî±Çá¬ÊÕÕ‡Ÿ­™°hÕ±åî*Êe3À8¸Öé´ Ã˜Úá$ƒ]áÅ\këÀ= ×s‹óø¤<ÚÉzXwº_ÖÁº‚hùÄîu‡™<iÒ¤ØlH':+U“5ë vÓîí³Ò‹^£:ÿ1ùyç/ÿå•38®ó�à¼åtîꈮþ&0ü6�>ž€¢FË[#~�dðÛ¶úûg©7à›Ôµn‹zõôò(ø×ù{ã–ú3<¢ëÏÑwWðùÌ þý1Pg,�ê´ŸëZ9܃Kã:§/K”PþÏ[Æû^Ìjó虜þ}mãÿs ~ÛS¿�ÐpÁ't4^¢Á8J U(-¸ó�ª Ä�—{åþ‚7ï3áÎ ÜÉ�2-$˜CË�°® twĹŠì~N%À†Â}�´;€,÷`bãó'Uˆµh+©î¬c!}�Un¬ñs±�Íœ�¯êÒæ£-4¨ðŽBš®“,ß(Ëm–̵ËÜj÷ ×çUȵ3Œ¹À ÔFµ¦àSrïƒx›ò­%w\ޝuæƒwàn]„Gƒrî¬B·{¬�û­)‡Ùús³Ý'ÛœõïYªX� ¼ ¬s½òò}éE¯�Èòò?|¥ã:ÿ�¨ÿ´.NuþßVNçòÔˆ„ßÁ*>Ž[_ÿ|MêdsÝ÷Zׂm†�/N#ºþžXÞŽ'® ¿?rsr¨o¢½XÔ_‡ð™ ‡ž¯6ÓÚøs¹FKÆÚp¢K­bûãÌ1PÂ@)s,h“OêXÀÁ'Pãv‚,`V…ÜÊOiÚ’wêXЬ÷iLscœ6¹£œMޤ¾8¦�.ÃŽÁdæË[Åœ8™Ú�x ”#*ìÚ¸\¯�” Ñ@ÓœTªÂúáÞ‚Uãâœú\È7÷W^-'œë 9ÓÆ�×�W` èA­yÊ»ÖÜi\�ê~k!lXæz�v!Æ`m{_h2�‚ä,ÀÅ9·.Ú³vYÌ} Ù†À¶¶\s�lC�{À=C޾ù�XÏUUk›�ž©ö€¿÷­AæX/˜ÿëw+â|ûzäViÈ(¡l0¹ kt`¡ÍP­�y†ð¯1�±,/`è¯òy´íX Vømè?§ 1é°"]Ìþjä øó´Fg3é‰åà¦m4œç¦ëïë¡ü»3Ô5²…ßûÁVÿbE«-Xl^Ûø¿ÐóC¨vÁ�Pï–ÝR Ä€Fpô¸… +ÜÁWÔqBŠ1ÝsÏ=Q¹ô� ®+ˆÑrR…sOq$„XÛòsµ!,˜[ Á¯ë@—<V€7uêÔ8ÎGî-—Dºp:~ˆËilÞ“ F�‰C >‰R�‹£,\›c ³Ð·‚W­Àµ{.HôÕ¼¹Ï*Z ¹¤ Y•hó$î®þ?¸þ›—½ìeQpJ¡,y¸Â §OŸÀkc€kÍqV L­1€åÈr§�«|hι¹YO¤Áµï¹ïZ/ãÓŸ1‚_¬=¸ü^}ZÏã�?>Ú•s­_›3f̈g�UµÝž(ÑyüU÷‘H)Ÿy?Ïý­PèµÆtþC>XAl\KnéˆA蜌äkß­[`í3ü6‹;nS»B!çu…t8~Õ�°ÁòçidN2jdYC]Z("£´dõwÝcÈ­6óÜûû÷@…?Bõ;Ô�œÌ“‹ë¬³NÀ£c|äÄ-âÛ‹/¾x~X0ÇU¨.˜­>r 2 Vœ °‚RÐå<Z Ç•õ>Àä¶‚:n-ˆž¬=p ¶�˜Ò+¯Uûà”ÃÊi®ì:cÿüç?- –+ HÁ¤ñ}Ð+$›£é>¡ß\aN2—¨‚Jívc2€®À§X÷ßwÀ4÷›sÍuv–ñV[mããVÛ�ôªGçù¾ú’OìÚ<fÊzûªŠµÐoãSô |+¶¥8—*×ξꪫH­¿‚c›l²I<3 ^æ¡J·µ²~Üa޹g|…k E×F¾ñò»ÐíîZî7÷·0ViÐjÔ ÅÓO·c�AøŸ×ÔÈ– ¿ „"|°ÃoÓùÏËò,¶4ª jæu¹�+"Tq7ˆÁë–X¯‰ƒ$j¤7�¼Ø4®…K¢¨Ù‹Amý›Ðß¯Úø÷®ÂŸ[ÖÊým.AøpTÁˆh@ ¦[€ÃžËÅŽ{ä‘G6×_}T>]B’¹”B£Êr�T€z�næ;×sIAXä\Nœ8qþqE@� ÚtœWcB½ï¾ûH‚8c;î¸ãÂyñÜZ°ËÉ&³—1p€]f��S î¸Ú`Þ†�7|rvA£Ða+Àv-žÌñ˜œpk`-ë¤Í�äûï�þ…6ŸuÖYÀî¨hEÂ@:hÝl³ÍÒ�´k¡ã\asåŠ ["îÙT°.œ[knÌÀÜæ‚� @ìy˜c›$d¨{ÜkkaÜéìð,€Ö-°k�Á1N¸ô¢Ó@3(�ëÇËR¡sE(+æF ˆ0Ý¡�¿©9+Ðåì>RezÆç¬à*ȃ¥‚ìâ”›zm8lmV§(mÔù»|ÇA>΃ÖhéïÃr{ל–þ~Ùùû³/ÿ_ÙBTÖ3þܾZ© š�)`JrTUx=]y¬� ø‚ «À(ŠrMó Y°ÆÕäHº¼rX}ƽÀB|Á$÷òàƒwT~0�¡úîüå'¿̉U5Z¥ã›o¾9@‘+ë<[í 'æ²kaÊÆ>�‰«jÌ XÎ+ Õ¾°lÐïgó„Ö� ƒ~p þô͵õ°€Õ…f› 8ä +ŽÌm¢�Ä››po0mÜàÛF�Ðõ9Xåt;ÂI´õ2/ еÅeæV{6�(˜åœ›·ùšg�Sl~6!ÏÄÁç�kÏú}ëâïY/÷{- n3ôyQŸ—†¼†Z¦¾þÇcÔ )�8!}Ô�ÀÎØ!v6ò`rjFvUAþy×ñ]ËKC~SÓ[X›Áè¾/Jñgµ´°Æµ�GWñ«Eê™�Ó¾þ»ß†û[ V�ˆq9³š38VÌõvËõBl½„ s‰Á™JÈ�Z'a¶œMy¨é:Ê9“®Iˆãðr.9¿œÚ Û�`5èŸ>÷Ó�I.1°u|P<ôÐCú|ô¸˜ 9=} 'YH2@æþ‚Nð(ÐËÿu 8©^rd¥âÞ y6o÷ £îÏhðé^ó Ç@q€Á±�óÊPrãÐ6�8×›nºi¬±Ïm�× ½v,’"^úthéÖ ß–/-·ÄÊØœpÏÆf�wÝ7¾ñ�=ñÚ°)�ô¹ÂÂÉmTö|¬Óâ´¨*ÑCHcœ³;À¯¡XÍwÌ­":¦ÿù*ÿ�mÙ¦Ä@Dôv†æ`×óƒØ×XÎGµQôgyë–~ÀÊPÊquLÒ€«�z CipE¨­#úúg¡­üßRËê÷1H$”@^9¢òWULDŠ1y‚'!ÐÂ~�"h¶ ã &D[œÍ}öÙ'À‘³ … ;§–+ÉÆ{úé§ÿ?öî5Öîê¼óøv“¶3R;•i3šÎK#u¦©"¥1RE”‘ÚÆ4 ·4C ·bÈq®ÆàsŒÃ%@°8®‡p‰ãÁ´rQŽR)SÒ‘‚•¤/¦�©�V�W±ÔŽ4�7ñè³ú<›ísÙ—µoç<�´uÎþ_ÖzÖúoïïú=Ï³ŠŠ¹ÿþÙL}9í‚mª+Ðþ»ÿùwEi XªúÁ^xáÉ0nãq ÝB�Á"¿ùN®c ™Šë¾©Ò@žZ ’½z¬-÷�@`)@\±+sRâ2wru©×�V¨µÂZ±Ï/åØ"𔋠Ìù£x8Ÿ��-ê/5=”l Š� ¯ŽŠÏúf¶vн�͇Ppn�Á¢�E ~Qî £0G3ÀyÚ†µiPDW²^€vk¥/„¾°œµÌ64g5á¼µÂmòvÛM#ü†ÍO¸bÓ¾¯è0¶"Ú<¥Å–¶ô ±[¦t›²Öû½Q,Cc×¶Z ù¶>þÖ¨ � ÿ�¬o�ø¥¢L�( YŽ(ÅU,j$%’’+tV.«Ð]`KµÀwÝuW�/í2ÀIé®î¡€‚Kª§í‚€Ùüü| ·–K tgff ø À‘öËžT`!Ñ7\Cë²Ë.+y¶ÀÔVKŠFQ;|R¸JÑ(× +¦hƒo` 6©ÛÚ¥xkOߎ©2 ¾)Ð�š 8º—Šm\ ]*ðv½77ælò\ZPàÊXŒ H›_ÐLç %Ù\ƒP÷yñM®°ùò;Už Ò=¡Þ€–ià þù©à–¹ð #§ÙïÆ–Ýg1Â"…Åó–üø¹½Ñ†³ZP8.ëÞ'°=£�Üå¾´,6ptZsM ·b=Œíަ~Ã.š’¢5³máÑ5ÛœäúÕ¬Ÿh—Qîg\Û¶LYÔËF³ �íÎÆ¥gñ« ¶�`Î�+wÏž=­çž{®€-å‘Iõ[B c„ø‚;¡´Â��*ˆÇ®—ßK½‡±‡.�—ú¸My¼à¨Q™ù�Z¤jÓà „3�)ŸW¨¯ŸB©ª�–‚ ú„VV ôÀ;@Ô.è¥äRZÁ¹m�Œ‰âêwj®ëÙšì^aÆÆ n�OŽ1P¦S¸…j ¹—¶ ¢ÛâˆÒ̡ɮµ�ØõÈÁ¿±Éø”]~šÀ,Y®³ã±Á¡Ýž ð6^ÇÌ­g¨ ˜ðî¸Æb€í«Òò,¨¿ ]n·yÞ|o5…®Zm‹"iʆ¿sÍ«S!ÝÔ�µ¬—ƒ~iŸï¡ØÐ|¥qn3lÔÞîh=Ào«ùLS‘¡ƒ•¶)Ú2„ðêùæs±Üß‹¹!|�ìÅÿmCŠ�™o>?+ý}¬©åþÀ“i¹õQ÷V«öB¯ÿ–k,š'�Éú`ÐP�%“/ Ê�سÏ>[àÖ²�—¢J5TÕ™Z+ÌùÞ{ï-¡ÆÂ��XþØÇ>VòT+ØÂrPŸk…,ËÚ`L!'à©b4ÒOʧëô©¹À O«6Á18Ë –· º6 Ô…€ qvNQÅšZʨ¬T]j,t½{ù @)µ`ÜB�Àfl<ößu�bs0A'õ˜bËg*0èt?u=H67æ€;d)¿à×vNÎSÞ…?`ÛD�Y 7ÝtSñEen×ðÇ<èÃÜ©\mnÜË'  Ÿ¤µãy q76Ï8 Lu‹ ÁÎj~¹[h¾Äͯ’×t¬9wzÅ/yÝþ'5(HöZiw¹PÙŶ‚•^�_ŠÇ>U{»£õ¿a‹MØû´l_²µ�àAþÍׄ©…¶h‰•>ŠgT¬tÝË¢Rmx<Ú6æå¾·ÿ}¬µP˜*ðdZn}Ô½ÕÉ^ÿ?ôûÑb>ãáÙ@!ÐòVÁ+ÅØ[Ǽ·Íx•J¹† DÙå—_^ xZà%ßUn¯ðh¢„àRŠÝK5¥bR<õ à(·ÂvA-uˆƒY�Jŵ-øŽÜT¿;¯š³Ð]@Î'ýR™ùBÙŒbUúÓ®|XJ75¤ƒüÜØ÷�Q/� FUb¦jËÓš”a}ºÖöBrxÁ·ÐbóeLæü‚KЬ¢ZB§õ<…?—kÜ£-NE¦ p°jÜÚà€WhË=`ì³q¸Æ=”nPLáõÜä8óǼEuk÷Ÿ9Ú-?Úƒœd Úðœ-ÄxÒ6„ÕÈo ›k�§[[Z’{µQ)¤ýôsú2yÂók¼&Ájow´Þà7, x\Ûõc/õùl7W\0ÛÙ¼ºýrxl¼–m©¼�4×cèü|Å(ƒi®ó0�vl™(¨NuRþÎ/³è¯Z¶\Û�¯Ó×è«Æ|õò7¬ÆöG©þÑ �� ŠêIÙ¥’ÆÞ¼€ˆ2(…Ó¾ûÝï.{ÒR"®|^!·LN/� 6Á"uXÙ³VȲ~¨�r‡�,(Vì ¼Ù®ÇyaÕ|á0 Œ…;.T™Új "j3`³‘�eÐ ð@7xÔ·|]¿óUf Á&¨¶'®vÁ"ðÌ”\׹ƅ2›sâZ×�l*4Hjü«6 ðù£MP ÌA:ÿâ>@ì^ *4?ðÀx)¿ryÝCÁÚ ‘™cþGñÅ Tßÿý­_ûÕ_+}¹^û”f/ k‘!Š{y~Tbc4ç Ú9ª¶g[UwÚ†±Z_îŽõ ³µþs�Osîc/V»âíz…ß°€³iªæz°U°ŸªëËÙÂ�Ÿ‡QaMh\-*f5;Zéßζ ò·kl~…¿íê~ͨ�bÇV¨½Ñ«u»èŸáÏn}p¨¿ äÊ{¥ÔÊ U•¤=øàƒ%o•º n…F·(°D¶U’­t伂Q¡Æ€*¶ýQIÀ�+F¿^àT{ª�3ðÖ_„+ó‡êɨ¢ÔèsÏ=·œê D�ŤL}Smµú¨ @OòwùÊ K™ÅüÖÖ�~üãÒ§ÂRàÚµÀœJ jµÒå*S–�³ãŒ ؼQg©ÆæˆòIûŽSŽCÅ�½—…=j~SÄÁ°…¡Ìgžyfñ×b�¶<ª2ß(à`ÿûÿõûÅG}F¸º0rc3/GŽ)áá|ÝÎ{n*NSŸõïù Û¶è`}6Ü›J𺷣­ÿ¿òÚŽÛ‹õûÅÿx¥/Ý~)4iK³ÅÌÁu¬¨Ô®x{ÑÊs›k¾øNË—ž^÷Æ­-2ÈBÁR¥ÏS7êN­ãƒF»ôú÷¸óoùicN¥ØHÖmí‡|ýY�¿­Ý.ü úï?ó»‡l}+À� è�D0m%DE§ÔF +,Zn¨­‘>ùÉO–"OÀhß¾}¥`˜ Pmsä:`HI¤ qV¬Ji°{ç�wZQ½Y.µòÆo,ð.™¢r éµï-¶­8£p^ûêòYÞ«lOÁb  £â2èãÆ­oê,˜¦0ƒ½ÿû/ÿRÆÖc{%×RE…{ƒ[>F!Ä|^̽À¥…�Ç�4ðÕ'ß´£mÀ¤)»Ælq�²,·×xÀ©Ü]j­E�œÍ3…˜O�Zx:zmÎ]ðmÞ¨éB½�ÝsPüÊ‚„‚^r‡µoì_¬Oðm1Á¼ðwìù›Ö½--’ÛžtQÇ—ªø�hÐUÎù1Ýp­ðÉÙ&ŒôDÛV3³C‡m©Xñv¥<Ç~íĈ^ƒØ±¶œÕiøÔívFµÂŸ�Vøò_kÛ˜Õžšé!óþ�kÿÛYC`¹âa�Ë3q4V3­'my«‘7ÝÍ¿íÿþSý²õL)@ ìÆ^» ˆQ�…6SKm5$·Q))�”aŠ!H¢4;!Ìrr˺öò•; ¬A p^×�´Ø¾G_¶ó¡² �Ï`è?Ê(ˆ{îûò—¿\Úh”j †…&Ë[¦ó•º FËŠí}äÉêCh±s±#çYø/°ùµÁ;¥Tî.˜Îƨ/‹Tc׃{*¬~�9å•‚JŸ PRSµ­o U©¶ß�43o sym(&·WŸ@ÖØ�õ-·ÜRî7!ßæØRƒ-B�j·�®çûüFÈö7¿ùÍÖÞ½{ ¼›saîr�A2õš�*d›;Ç[mÕ¡ÓÒš?æí_ª¢‚éi}NP|½r…áµlXû-Î6|°mÿÕx½ÙÆ-o#Þ9ÖÀI<šT鶪s­ðÙ‹Pµþ¯ö·§Öxk…A^´B �T™&Dz¨Õh¬Æ"Ãj‹_Ýœ_ËŽMY]ˆ©´¾d:ê,�à‡b "¿ð…/Д' z©²`�9j$ ¥$~üã/á¹”N`º°°P”CÕž8PÀKÞ.¸£ ÛWn+¨¤˜R<A Ží–йÀ” eÖ_ŸX`È'*¦c ¸^ ¨A>�õTMj�¿0f0éþS^‘–úð”hð/ì´]}˜Pì~ ³1Û«—ZK�µ¥“>ø ‚-8¸Ï{Ðj)à|ŠâVB�=Š6ßUئ‚ ƒ¨rw©óÚú,¼ÜüjßX(éÂÃ�»yò ùh‘Aæüå—_.¹Ã1Œlóƒù0sË߄ߴ!X»rZ»Âp7vl ¡g[ÛÀ8Æû²®w@Æ–9ÓfÇÛö†>cBax¶ Õ¿Ö¿Õ΢~^?=¥Õþl5�®5Þ¬»q,#Fc5”ÖZŒôߪ¿#°ž�Œ�(?m™à�,,—zë½0g` œÀ•Rˆ-@… J¨/%[ÀÚ€!ØÒ¶�a0§ˆ”°[`Élëãz¡ÑÔKÊ%0æ`NŸ �" n�“+|˜¢ B�Zì7 æ›÷ �ïÆ£-@í^×PD2¸7¾‚UJ²PacÑŽ÷�›_Ì=ü¥ -6î7>íI- Ú>ÅÂÁ¸Ò·{AûóÏ?_Æç(ï|¾¶Yâ ˜ì@Ùuàø©§ž*P«‚¶~,TXT�ÿk‘o,ÇÙ=”^ó î=WÏK-áå ø¢…³£ö̋푌ßg#á7­‚mnS>Ú½ãVA'!Dms3O6óòäFe¥pX7v¬ †×Úþg”ÖMxs­°øI²Õ>—µÆ›*ÐÆ°\è�Õ¨°u�ÿ™ÿ;Ö�ðj+ðŽ|÷Ýw—JÉ`•ì:j¢Ê”ZÀë>@åw £°g /8•_*´Yè,¥R¸´ÂVò„åÁªV,4— Ç…@QX4ÀœfP بš�Ô /–­ЮO[ú€?À�©©îö ð£r2Ÿ©Ôr}©šrpC9v�pi`ë°¥ªº? –r ¨Í´ã§1iØêÃ5Šw½òÊ+EÝ5¿B¹]ï§ö…5ÛçXû|¡FÛØœòKþ2�¥¦ƒaíRÙ…D{>r…£ª³Pls!œ9朻O»ªdSu-�^> ËöR`‹ZíYd°�aÀÏ'žx¢<£ÌÿMëÃfÛšC¥ ås’¾</Là—Ò™fÞF> Ûœ*ð²ÊðÎe Ñ�ÃÖú·ڗÅiµÕÆTëoV;Ú6¬ôš´åm˜*ð ÛÍÅ�ÑXϤvBåYB“…Ã*P[äPo)ƒÀ”Ò+t–jŠ-ÈcîeQ@°€%p©Bx�²ŠÒ¶+Rì dÉg¥ ƒ_ÆJ¨fJ*U(«È #(¤àÔ½€Õ=Æ4å½:{î‚`àç^ðí½q;ﺨ …JG•gÇ]ŒåÛüºßPiA¨ñþØCT+ôV?rþG ôºˆÊÓµÀ`¼®Ó¦Ü`s¥/¡ÊæÕ=æ4:t¨(½ÆOQW‘ZX6H¶h¾…ˆ¿ß-XPÐŽ÷´Eõõ,ø!ÄÛ£v(Ðr�åïÞ½»<7EÊZM´@{èu¢ /œ8qbÓ�_½ì…;­Ö »'Ú`w”Ì^öâ¥mkË^O6³ŽîZ¶´Lº¹F,l[ãKßjç¦ÙV×jpÜ‹%�o Ëç<Z[¬°�½Òbs†?O‰õU‹ ö (©¥òqíñ j… ¿¥dÚwð Ó¥¾RÁµ” 7w@h0*±0\à§�•öž}öÙ²—­0``À<-�ÓnÀ¤¾…8° @½@v€‡¡ðj‡Rl<o|ÒX¦S\ùB)…‡.y´@—zêZ/í›ã@XÛ~ןó, ˜ão5€HQ¥*ûꎩڎÐ5& ®}Ðo¾(×Î[0GB µ}ýõ×%×À/ÅÜVQÚnn¨óúÑ6…Ø¢�ù»úê«KH7`÷¾ßøÆ7Z/¼ðBQŽ]oåSüÁ³çoÁÃ3Ñg+‹`¥ý«mY¡êñ´Àîr¶Tq?ÑaØÁªóN‹ÕÜWx£Xl©U؇ýÅj5è[íÜ4ÛJ�\ËR Ú–¡î£·AÃŒ·¯ðï%e¸;žÑ�£³®¸SÑB”D¯£G�–êÆ ‹ vÁ%¬ªž|ë­·P¥V%!Ò{öì)Ð ’U¦HR1gffJeem) Ee¤² ¶�OTF„ÀWá'àE¹ƒB|åúR)©­ ¼F(p |µ½í½�� �IDAT)¸®“«l< U81Õ7ª.CÊ(¨Ó�vÜØŒÄ‚Rý:nl|–ršµ |½6B)¦ÞHm‚kÕ®ý�ùÀ'J¶ù¥®ƒpíèCÛÀ›[Q¾]g<ÆzÕUWô›O~›k÷êCq.÷Sv)Þ„LËËFmÑ‚ÒkÁÂø@µqñðšcвgO¹¦üc!ì@~{>pî ¼¡,ÂVßZÇûÞ.6U'Õ¶¯3ÞºÁ«BjG>}ˆ_°VRDÒÒ†mË�È´X­ýìÓz³¡Æ�‹øƒn”áÏ#´®�Xo ‘ÔO€#tYA*Êo¨£Â Á¯Ð^/�f{# œ\g  ê@¬Ð^m oYÀ•IU´…�¾"„ÄýùŸÿy Å¥LR2�%EWÿÀR®,ÀÖ6€»±/¯±¸¸:®=à ªùª�}èC4cÛ! H©u]„8:�蠺ƭŸ¯ýë%´Äkpó ¤`mQTÙáÞƒiJ•”‹©ËæÛü�|s£}óO}��ô Õ�ßrªA²Ü`÷_ã²ýx5¿ûæö•g öµÉEÌÌ‘óB½©ÅÆb‘Aq3 Ætù¤·ö©Òr‰Í‡Å pOÉ7ö„ß c››‚L‘¿»Þ‹-6@1©«÷Û×Y8t7Õ†§Íf×xý´£Êñ �¹Ô,Ü £ÈÊz s^Ͳ@ÛdØ´pÚx潆 Ünþ<Eöε\/öØ”�Sn)àoTÂë®»®ZM^Q 8�$a¹rxÁ±ðc9½”C*'L@Qˆ.PSšâ©Ãîe h©Tì:Ð ŸŸTXjeTs¦Dºžz ùÒ@10£HS;¢TúiŒÚŽ¢\®îU¦]Á¸}�¡v�³p`}™°M�¦8h~PdÁ·ðaj-˜u­vÝï˜ ÐL¡,!ÉöTv.ÆHU6WàÛ¢¥® �œ [éÃ\i›Ÿæž:N¡’m<~ Œ£2P¸Í¹çmaÁ¢ƒy2§Ti?£b´~-j˜3¡Û¶·øîÙæh§m[oŠc·¶ÔlO33¡‹6ÿ©®…aK3Ï“P‰»{«âçbK¥—�M[£JCXJXL¢M3�§ú;>[p‘¸óïç ”Kþ<ZëJUÀH±#0{ñÅ€À�jI1þK%k ø�HP°�ÅÓ¾´€�LýŒ­}(¨à”�hà æ„åÊw• ¸Ü'lؾÃ`,»䩾Ly—TVþñäé'€“oaàŽZJ©{ÈRé˜ 8]ï<ˆ5Nj)t=…p˜×—|gëE‰¥Þ a6—Q Øjt I¥Zû@äs~›ª7³Ý”*Ùª]Ûj‰z ¼½�*ÒÞƒp~Rà]ÇïùaAÃq€kq@{¶:²@a®)¹ÚRÀ xÇ~À€Þƒç§_ÏÃü™ª>ÕØ|D޲…‹Ì^÷6;bø]jrϘ ÿ058öj�¤/5ëIžÆÈ‚šŸ…šaÆãª½¬–ª·Uõ~,à´~lPèÜÜÁƒ¤éäÖG#¶5à€…Ÿ¨¡”C ˆŠò«ˆ•m|�8Q@–’˜.¹ä’’“ ¨€*¥ØVH~RmƒD)~ôÑG RB)ˆB|Á"h ¸À°ÐgaÂÔKyÁrXA¢|YùÇ`œ/Âw�÷w¿ûÝâ¯ß©”ÔX!Ý@“’Í&¿è[ŨÀ¢6\C�Õ&ØàÚ„ÔNáÈÀ�¬ïØ7Ì ¤óYÿ?÷Žw”y¥›�Ø3,Q0Îø $ùþlöަ  Í'ó ²í-lAB΃^¡ÑTg÷?óÌ3eþ(ä €2´�Ú=ÎÙ’ ܃  žÏùÊ”ó)š¢Þyç•Ðs×3ÈŽ Ø|M[·63¢Eñe}ÒÕ¿cmûµ¶Úàsûs$gšù[/_´Nx²N;¶ŒRЯÕTl��P™­µ0ÊÊÖƒX­kµÿi²~`v𣠀Çk þ]ÝÖ@t�í�ÒFh]Á�BA(E�B ªaŸòrå÷‚^J©°[ €lgDÝ=÷ÜsKÞ/à– |ʘÔDàæ=õWn1pTŘRzíµ×p¥pÚÚh‚`}�E×R¨©±®ì”UE¢ÜG æ'ÅÕ5B–)ƾMõÔV«ÙZ ´\ã¥äÌØ3—ï€Ò9ãv¨j‡jų@ º~Ãÿ§É?6&ǨØà™Êk.A«^>ÊWv¿Ðc~™cÕ²åþäZ˜0&þF!2cQ•ûk_ûZ™ï‡~¸Ì8å+%×<óUx3€¥Jzíª$ý‰O|¢„;{ö`û÷ÿ÷Ë܇çåùk#ªa›g ­&|Þ{þeðº´-C„ßö ¶›Ú¶x™6 ¿ÏhÆqZÛ~­£üoܤŽWïö)« ^ó îÖÊ )£ŠT¨ÕÏ´(}µÆ» ‹õ³¹MZZ¿6hŠPäò\ÌPøÑÛš p«PÀ¨¢àMج0h…ªÀ(µ“:IåSà—â œ©¥”Z ä½s`Éu®êKM¤8‚À}ûö•ëõ)WØõ ‘ÒKMž,ì6`/¶?’‹ z© à >í‚cÀ§­§žzªÜKÙ¬àÐ;Ç3f}Fá,!Ä”h�JíÕ'覟üä'ËØø.ÜTÇÖJúö»|Ù(*åZ� œÍ›÷€ ƒÝ8æ>�L±5NÇù&¼\´ùˆÜ`c îÊóuÌüIŽ¢`!�«=›-BÈ Ö—… ê¯ñ1ÃæÂ˜µG ?pà@ÙÉ37÷òÀ©Áæ|×®]E6. QÕZÛž©yO[·V»(ÑBóŸQ/!IÓúv|�ßÞ|ñÛ<¤�åq*$±mÔRûÅvkO6!çÓ`µÃôgšÞVós±Ú¸ÕÎõbÓ:“6ÞÍM.úææol»ª¼4aêÓÖý©õ7e\–à3~[`1Kó™äßjª¿c°5àPîÀ�ŒWB ,/tEþ(ÅP »¢B£…6S6�u€�gÕ’U)¦À‚I ç=�s/“ÒH±V€ô«_ýjCj°c@/F@rÁ³6¢XVˆ1õDÁ‡~l,ÀR˜u(ÄÂ�A%à�dǵŨ·ú6ŠoQšcÿÛ(Š.´{(¨ÆˆõËŸÈù²Q�š_Æ£ •sæÀ:¨Ý †….›O@®-@+ÜÜ"„�t¡Ôú¢ºûÉ@¿ÅˆØ‰‚l~�E+jº92î᯽÷îÝ[Ú§:S“-\€ëçž{®@±çà^×ñQÞt83vç3xÝÙæŠªbTSÞÙ,¬—Â:G—Q½ç*†?Žkž¢êð±Uà¿ÛÒÇBÁ¦½:íXex¦’2¸¥òçbµ»«�ëÅj+àòš p nß³t{Û^ì;öh?ÑTï¬N>JÀìu¼ëm‹½´Ñ[�jÐ ÀSfk0p x‘Ÿ šÀ˜’ +¬öŠ+®(aÍ &�S t©¯ÀÉ~¿*Sá³ D[Š-€F (@bÂmwïÞ]�MŸÀJufáÒ¤Á5å1öí¥vjCµLú F^× áš@]¡,�ûóp ­BSüÑH™üÔ?¨µ=P5' �‚V€&åËz¯]× Wf _Ø4µÔüDa+ï¢ñʽµÀ Eªäõšcsg^\ÔU¢¦œkG_ü}ä‘GŠºK=—OmO_ê­<_à*D™ ­­óÏ?¿„qó!*S“ñiüÊ+v/¨¦Tk“ nÌž+Ù”bÅÍl±äÙ«Z}Çw”ëâsÓ¿ ÂëÆj)Ç@êç ãæu^$f¾­¨Ö 6�^ZæÙ.T„ÁiÚ©¶ üR…±×L_X ò�W\Ì™†ý k.zÔˆé¥�Í€|°m;®Qü{ÛÚÔnÏýÁ'Ú¦EÝtK¤Aþ�æÞ¿c²U˜’ p@�dÂt`º¬PHª@‰J )±`XF¨1È„î׎÷ ‹j˜Á-EY”^Š/à¦ÄºĪÌL1î ²@œ6©¤ 1é@Æ^½ W®phj.UXû€ÐOP(ô×y>Qj½�ÊÏ 9ªF=À ©°~Rq…<D�(ô×x´Ço}Dn¬¹ÖúqàzKã¸*WS^ù¯H—{c?bs ÐÍ?àïwÏÆÂ‚1Y�à‡y7'ò‚-$X<à³ùŽ<mÏÕbƒŸ ÙP�_ãŽâeüèü�|hcÔžþþè�þ¨D¸ïµ×^;¹íTÚºµZ*Ì Šà$~ñ™éPNžlSVú…Ðù)Üa9ømUV�7�¨�[ «ýü|–^à35[Y9ëf|µTŽApŸ™7›�o Yé¬õÌ· ø»VåôÅ5¾¨×„�'»xÆuë½�h£Xx— ›êï˜lU�f� ´�Gp’(«B˜ ¤t‚Fðãzù¢ÂcÂÚ±cG<*ª{(›à üÆžµàL±%`¥ð’\ãÈùÕYÈíÎ�;‹”N0Fùô\~‚aÇÁ¥û„Q ‹FàÊøAí½à‚ Š_îS° H‚zª4•9¶£ð˜ÁPç�"X¯æ$®�^?…Aó™9ïw .ÀåO„Bƒe�ëå–_Ï?ÿ|pó¥˜U„eЯž�Â\[×EÕlþ k6ÇÚòŒ¨´#,.€S‹îó,Œ_encðæóÈ‘#š©ÄÚå«g$ÄÝsv�ÜbsâùëSjªµç°œ%¯«Àƒ¨B£àí_–v„žh¾¸µ+'í>ò%vÐ/Ó£\‰_ ~ÃjªÀ3S?ŒUþ­ Ìõòï`s µºùWó‹Þë}ü ÚÜqß–U”Îx½¹ $wk5Ç{°Ï‹^}^ÍÖOÍÏw,T\æ9Ï4Ï1áwãØ(þÆ/Ž¡âzÍâ�i=Úš�Lµ¤æ‚+ÐÁ�=x£²0À¢�SUšKɈ£�Å’šî@@ŽLÐç!ÂQð T�1!Éà[�%aÀ@\y¹�jO¨3•4*@óIH¯÷üÀB°˜—{ŒI®°<`° ÁŸqø]Û�—TSê+˜Ž½t�&”ï Pê'àV¿àÖøùÖjB€%¨“ÔSaÉ®�b^‡.c÷Ò^äk¨š7ª¬ñÄVI�ո̳c°�”bYæÎx"|Û3õ;H÷Œ@vä)ó‹ì™Ëÿ�pmc³°aü5€´ù§›cm(ˆ%,=r«Í]ZÚÖïÊî“#ŸÈ‘{©ãËr¯_,�µAFõûZðÛª¬·šÏ¤۠!v+Ùæfü?]CÕ�m¼•®é׺ýâX³ØRÀl·cÙÖ�U¯ÿ޶.É'Ú J­fƒV—í´—zø›S{¡£›/êÃãœmSìÛ³êótX­Ïÿ¨ò¼G½oîý;Fë €å‹%ª,ð[ÞShmUž€xsôQ+… ÷µg, ŒŽ�% Kñüö·¿]ö’�-z¨£@�ŠÅ¡€–{5XÕi}�?×PY¨{ùëE›ò�AŸkÁ¢±€oç�š¾ä;ç%p*|}_ã©àWB|Á+(õžòëzE¢€7µ<ò“�cÀ¨oó¥½È¿u?¿÷½ï¸¥zóÕIæÜuQyÚ�gX©Ê”a÷;§–PæØ‡Y;æHŸž‹¹“C nù O`½¸¸X~hª0…wnn®T¹æ³çeò½Í‹ù¦ôßÿýÅ…¹<gÊs«)€•ù¿ikX?_d^±ú[3|µŸÜÍ-¾ð�€»ùÒSSÞ6%…p†ùegó �âÕÏ‚M·ÖËbFÍ9È{s…ðå-m�5H¸ør6ßåç·öÖmÛ;–ƒù™!-tt3Þ¬fœÖi5óàŸìø7¾¥#ͨƢȨóqSý£­Àà ðù[Ù„ŠDå~ªâLí£¦€ ~€!(¤ÛƈZÕ�Nw~hî£ì(è>ð„T;F ¥: ¿¦L‚A Ò¨§‘/«�ظÕìåËWÊ20¤ÄR°Á\¨ŸŠIEêP“�§ëè™…³÷Þ½àÈó ì‚ä¨rí¸JËQdËqý¶š0hʳù3±å’ Ö¬/à TùæõÍ�깸ÆOPé>þšïÍ!…Z8·ãîÕžcž… ÓžeöÅ€¯Å�ùÝüQHÌÜ©ÆmÞÝk, _!-‹ò®9Ó§�iÛbéoaá_¿ç8Þn'„ÓÚì`ÿqmo”—QÃNÍÿ¤¶öøe<®$êøæ×V�§!x©RA³I²Å?[‹CXغBøò[+„Ðj½Œaa`‹�Êh¨£µ:º�^˜¶:i÷šŸý™Žãou¤ÕXàV¤Îrvl !×im¶"�·“ À0�:~Š}q£¢šR@u¸�-¡Ï¶BŠÚqÔL:µ0F¹¦¯¾òj:ûÍ‚fU›Á*�SpÉ=ÂpAö<P‚½À%¥N*pB)™úÙ€R¿`N(²ã€Œ`‘Q…ÁwìiËÀ€Ö>ÀÝ ‹ë�?r~�ÇœÈ7ùú» ß¸Æ¶K€7öYÖ®6�«Eyµ@Û‚€¶Ýg®õa.õ!ÖÆ M¯[o½µô'ßššì>{ëßs£àªÖüàƒ–g(´Ù~ÃBŸÁ;ðêL•§Þ»ÏsäŸ�lНyS\˹�~ô£åùPÅÁ¸Ükpïyv~žZ™¼^¬æî×›/nË�m€y«Q{Æ‘ïYû?Å­Íxž\EÉžmS·ó¤V™¬©÷³-Ò8l@4.;Þç~Äݪ§ëeÌ�rdº¬ÛçU³ÒwÚú°Qþ;¯U(kT�œêï˜mE�(ô¢zRWA¥4B�©«ÔS0DQ´õà”;Ûb{Ï �¥4*dE�ú Î�"à¥kK*AŸ}ÎÙ€µAEå ¥¸‚+¹§P Ê�±j°'œÙñøÃî´)—X[`œmp«Ýh�iP:0«Ù÷W˜7€uĹ‡Ò VÁ3EÚuÑX¡ü4ü4g‘— èÁ&…°»nÛ¶mÅ +Û¼‚LóªOÀ¡å`רs=˜u�y¥€ôàÁƒ¨Íß]§r·ç¨ð˜h¾*¸E!Ö‡"Y ´¥°xöìÁ¹Å…¯bŽ…@›[¶@µÐê¿üË¿,ÏÔwZ§"œ6µVû‹ÎÌ á›¯W¬b:ˆÕel5c~r™1Ÿ èÍr6 ßkXmxv b_G*ð|Ÿ0¿4ÁŸÉµlgc>6Åã]ìÑ÷üRŸÖn£Œ ¨ÀK#ò;óÇl«ÒH„Ó‚5�%Ÿ‚ÈØÒH.¯s”?aÍòHA'Ⱥ¬(°Ý¾}{Ùæˆª©úrT r LÁÐ_òoÁ˜¼^íP€…I= DõIaeªB;ÀÉàJ…¹�V{|¾-ôt«V!Ò`ŽêËÀ6°Ó'P�ÊÑàÒÞÇ*ZƒwÐH½¼ Ø|èÇ| 'ã±_²~Í[¨Åæ�ßú×®~Ì�÷€Ö}üq/¥—ïÆb!\Zp0~ã¥Î›í]9½¶#Ç333E-WµY1è5ï®æÆÂ„3¯gqÑE•ªÜTüßýÝß-ÛN�{Ï=÷ÜòlùC‘ö|U§–Ç­p<µX[H[·¶Ö–ëͦõK{¿�2*«©ožøèQØü€c˜ŸBXÄç¹)ïRj÷8ǘ9È“g5÷Ã^Ëj.~Nsïß °U¸Õ@0@ŽTG DáQBœ °Z’�°£F…eáÇ¡úQ$唂-j(ÕdQ…�!Èuˆ£<4€ xï¾ûîŽ`hñE(/Œ®w”G>è ˜J¢(ÑÚ¿Ôi°êmë;ÌïVŠ-øÔ˜^U¥µ …Z»ÎKî/p¦¾òÁ"AláZ®1SRA9Т òUi6&` £8—¹ Â ׺Ú4mÉ‘ÖO=õTiŸ@÷OþäOÊï€ÔÜPå�ÕïàÕÂ�µ×ü›Ðfó£ZwäE ¹þ³?û³²áŸ5„mS£Í�c¢þÌ3Ï”y÷|,dXˆ0iëÖj«wã´nÿãœt˜ì´cS 6ÖþÍLI…Øi¢°…JŸ«‹¦(döh…1ñožO¯ï–Ƥl-­ƒPóõj£ú;W€kWpï´Ì—Ÿ�[€…Ä†Ò ø�$0i`Iá$ ”�O(“L®*�ÌTSê/ÔõQ99 _¬€mÐwå•Wp£F>ôÐCEÝÔ/è˜ÚܲeKS¡ÌÔe ¨-áÙ@„R@ε ü´ 8¼P_ ù¿à¸øÓ´Eõòk>(ÎÀQtì‘ �µ£?}�GÀ ^C!ñÀÒ}ŽQy�¯ñ�Wóªc̱e“¹ñn¬Mskî�7ç(¨ï ÌT!æÆHÅ¥ØZŒ T›s'¼Ú\º×Oφò+Ÿ×X©ñBšÍ¡íž�»v…Àƒ^jü¡C‡Êsüâ¿XÆe|ªF+”EeN[×6 ùŒ5 Itûçñf‹ŸiX¹�¦/„µ?GÓP«Õ<Ÿiû"´ÐgÞïJvÖ@áb¥Kǧd¼Ç\œõÂè4ý]Þˆ6��ÜâBNîý;!¶*�ƒ«Pû@•W¨°]€øøã�—¼O/Ð&Ÿ•¢KÉu ŠÂ(¬–r^A%P``Qø3`|r„�!%WÞ©�`[$ ™¦ �,ø£âKm‚; K©q@Œ Õ忾à¤Ô‚DáÜ”X÷�K�[=iŒòldÇÀ0Ÿøª}÷[�­ÀÒqÇÌS‡Ú»woùIÑ5GRã¾ ™l\à\_|Ôßü0'æ,ò†õá¼d nœ(J²‚cÔéöPpp.—ƨ=ã7‡æ’‚ Víl~ù©ŸxVÔfó´ý¤0kË‚€gmO?ýtYP±[_Ï>ûìÉÏQÚº´ãc†¬Åæ‹O ˆè¶RìÒ|Ùêf/ÞI²Ú*ðÖUŠ‹Mš�5E_†æ*Ão« ^&u!`¾ñ¯–ïüÌ—*<�QV;?>eë6¢-Žèó^;çnX�œáÏb«0“[*øPÁõ /,`ŽÀ#˜ûÆ7¾q²Ò1Åж8®}”Zy¨ ˜ YvŒ)¼Ì©<¬ .`sM„ƒK Luˆ UÕbðÎÁœŸ@”ªÌ7*dlOD�¤lÆý~jS¿` à2�ÍôO±¥®¿yP¨JrTnË�•* V�/àòÀ°ºÆy`©-0mŒÚÑ>ã£9œQ� Ü«rízmÿÚ»ÞUŽY8° A•¾æšk ŒzÝu×]¥?>_¡BÄå '�­šøGýǫ̈ù<+ù½6(öª<{fž­E mF14snA®/�Þ9€²goLæ’ï!æ­Üx=Û±!|)îÆæÛ@´†¢ÒKØì±æKì$*9Gߦí á0Tài)Bpјþ ukñy–²3i) ä&ñ™­øwm~¡ÐKüw8íí67"è«©kK¤T'ÄV`€Gí¤Ä‚$ª"µV~lìI „�˜Œ”HÔ(„‘Ïûä“OeXþ¯½ƒ©…¶ÝXàNÞ+ˆ² RåƒLJ)p£HÆþ±Ցðb@l+÷kç@ àähã[«)bE9j ÔÅ’ ,¤×xÁ#_As€Ÿ1;�çX̃vßñÎw”Plãñ2WÚuŽËwÀh.©¯ Ý<X,�¿|ŠÂQæ�/”e‹ ûç6m*0i\Bš©êî¥àšw…À´©_pnÁ4ÅÞ=|7ßrŽ]cáAŠ˜e`m¡Ás7æÉ{Û QØ�%r�£8À5ò´…cË1ПûÜçÊ1U¸…M·²úóz·…ª¢¡P´1­¡õªÆ—¯I©ä!ÏMñêrM�Ú<E*p«ù�6a•A£Ò¨ cnBÔ<_POÁÕ…õ³– ëoÇÎ!.jLëBßFµQENÕƒ®ýosT¦Óº°U©$BWÁ¥0bJ!øùÌg>SÞS\HæK)½ä’K R_å‡Fá¾àö±Ç+  ¤b?\ïU+v½v€õ˜Rs4pTPÊû¨8-Ì™:J!¦ÌR'�©�t ÿ"ÏÕ5Qä)Ú�˜`P{VõÁ#`¤Æúé•Ôõ Ñy¢ùX—ÞZ*íP“Ã'ãtÎ=€ÈSµõ%¤�GÞ°¹Ö&…ÈS¶-€[PLaÊüw¿yÕ¶¹•Ã+WÚØWðË|úÝ–GÔh}{f ,\\|ñÅE‘w¯Å´„kcàÏ+¯¼RÚð\¨Î|Ðö¥—^z²Šµœá{ï½·|6Tƒ6Ç®³µ“ň?ýÓ?-��¶îmqÈ¡}±}ÌéËüRc¥vKŸÅ“v6>�K½ZjóaÚW–‡¡Oú¶Hí{Ìž6BÅd9[lûL�È›~ûÙnhP;Ú|Iï§øS¿ð9Ž¿!KmS‡õ·c®2XÇ¿‘i^èÛ¨6ŠÈ©Úï+û›[M�­À±Ð0`ŒâG ^€GÅaÊ%`C@‘r(ävÿþýåà§R0`RœIá$J%ø¥L AM*B �ÖWìå ¼�²pZP ò€Àò,oUñ+[ý�`ׂP¿ƒËØoÔX¯Å¿@¤êx:ÏÇ�Wj­{¤"`1VÊ)ð™BÂl¨ØúV°Ë5@@jH;oì®Õ–~øÉôA™u½6Œ‘�|Ð P3O|q+È·�2P¥äšóPÒ)èú×>ö ™“8ï�� �IDATÎ`ØüP|-`PåU•’~õÕW—PiJ¾oÏÕÜ›cŸ÷aùËTvσö€6Ç ÝüîÚµëä^ÊiëÞâË\ÍpÉ…6(X­ÍÕÎukýn¡_$75?‡ýE6ú;cL�2L«=wÓR«Ý"'ú´æÏ-³èSÛ¢Êñi Žû3êèYC^ 8ÚöowœEÉÚÿ† S9mµ)í£‚î£u–Ú>Ÿ ÓkKmÓj/2ÒÂUÍÏ[†?O�½s-WþéŸÿ©À p¥"‚=ûÅ …Uí„ kv \öü¥–>úè£ …Ä UE �2 Š@&`ýîw¿[ö“zÔW°ëx’¯ Ê®½öÚ¢"ƒÃç_x¡õþ3Ï,[ï|ìc+Û0�B}(¼D™ ¨:ÇW� :¸–GKUá¡4ƒ~ð Vßö@„³ØÚÇèÃu Øýî¥Âº? ×5`SH2¸5ÆLí�*ÕŒÏü´  ÷úImv�E¾Pf©îòn½×–ÅíY8ðdË÷5‡ ØB@„‡¯±ëÏq îóì¨ñ€Ú}æK»/øôÞ÷¾·�²g{ûLP˜¯¿þú×>à^è´ß=+¡ÚÚä+_F™ |âĉſ ņ±'Nô4Þ!(;Ö¶϶¶S³käe.¶}íuåuiBžu|¡Œ홎1Ïô¸JÝê½4Äÿ@wNHNâB~Á}›Å¿¥vPi_¨éuïãÅÈ«¹ó0,ü�ñ·ÿ{ÚÒC˜û±Ž;“<îøüÇ¿ý~Ÿwç–DãÎ±Ž½£ãÿ¯í§\õ¯Öî{·ÏjÔÿLúߪIþ-mŸ�m]FaÍw|&ýoXØb†ìO–­ À å7ÿóo°»ùæ›Kþ¨¢Gà,Q)“``)~ä 5ÔïòƒUoE ¤�IíP)Ä�<òH�G°gZÀ �AÓµÚ’BuAã×_{­õ_>ð�¢`Ê}¥:*<%t|ÊÖ6 k±Mh£Rj� 3vòU�7@r `ú]2àd”NsH…2ƒY@gìú4F~èÛµ€ÏõL›Ì±�U~™瀾±“>Í©kµcÌæ"¶›ÒŸk=!éæW¾²ñ¿B^`G•hmDN.¿µЯ áÑ ø�(J¯¼mj3Ðó4,X€[óì^€{Ï=÷”Eê¼¾-€ð(ßrË-%ŒZ%oŸ‹,„µ!­ý ÷zÙ7¸ëü‚´ç ­®u~†&}¯çšÖùïi’‹‡Õ²õö¼;aÒ6®-µ}¾'õÿÆ•izµT'ÌÞqç�wîkµZ«Æ¨`°£Xð•?*DH� *8® Ü(«€ÒïrDíéK9• U/VZè­-“b_a°áÐÂt…Tƒ9pé8¤`‚/à­-�K�rú�vÞÛÆÐQˆ� k…kè !d|”»êm¹_¿ <T_ÇäôjŸ_Æ,Ya0¿d@K¹¦FƒQý ~öâu½kŒÑK^°pese<q>†kÎÝK¥Õ¾œas�ܽö”lj-ðÖ†BT X.¶Åð ¬ÝãùPí-L�taÍž©±zO¥¶�‘vTx~â‰' €}>òÏ>ñ½ v?…Ú<X¤à‹¢i‘Ó ¸©÷¶bŠ}–û±M›6ýý¦M›žÞ@ÿ.GfòùÓÒÒÒÒÒÒÒÒÞfé»*MÉ•­Vë_N9š66[1¸Õ€/�â+Ï–zzÇwXˆ.u·Õ¨›€Ž:¶Às0´ N²ãg�uV©ê Æ€08£¶R©›@��NÀ ö@ð‹ Ëü¢ÒÚ‡Œ¹Ç5@è*ô$|�‚]à A¤{Á-(£ˆR8Á= ¦ÀºF1ÅÔߨ>í æåU;àÝ|¸ŽÊÌ~þ~¡Œ+r‡÷“æ…?@$Ç=€Óø@(˜ÖŸq�~ ¬Ê´k(éŠK©¸ ð�Ã"„y5Ç”`ÏÁur¤mObù.dìóø›cs(D[¡1*¼…�Å®<þ¸µX`>A¾ðsmð1 ~Ýwß}¥o‹$Âß�‹OAÒÒÒÒÒÒÒÒÒÒ¦ÄjÕ�Ƚ'ÐVàÝ¥øQ{ÁŽQ IX«}g�Ñw¾ó�Œ@ )ª k)œî}”M� ¢8zÔQa· Õv?®¡ ƒEPœ… ÛŒ�X}�, $èVtËõ�?þx »z̽B«µl)Í@ðñXFh  Œ½‹ÚÞàÜJ°ìx5& Â�&ÈF UWþó7¥¿_ü7¿xrdýéÇ{€ 0­q™o?~øá2^ê-ÐV Ë8ZMa2×{/¤[ˆ±âU šÒª}9½à��rt�ƒ  ½ß¹sg9ï9Z0�ØT@ 0PÞ¬â³þ̓ç乿s~z¾æAî7W,Ëø-V˜gÏC5pU¥õ£¿´´´´´´´´´´)°Ù¶ú%ƒZÖµ˜@[€�V¨°{öì)ðªØ‘ªÏ K�O!¹/½ôR"°è€�ãT߯~õ«%L �6`)èR%)ÆÔ_áÂÔE@º@1`Sò\AšcGŽ)Š+ø¦bº�Ùj ¨)´ÅÀ{ƒoþDþ�@àê¸óQ, �ÇV=ò�*¸×VÌ€¥€‚zyÉôrx#¿(k/c(¯ÿù—ðçØžÉÜ�DsÚõ#çöÅ_,~h!ß ”é/òg¶ß-"˜+> 5§Àûüò‰’./ _uÕUX©òÆL}þ‹¿ø‹¢äRyÍ­<_ÏÇ\ñÁ½€Xˆ5ÿ¾ùÍo86oüq |S‚-LhGq3ÏÑâ‚be”l*q„g§¥¥¥¥¥¥¥¥¥�ÈV+º¹š¬¨þæÞ¿j+æ�-*ð°ÂcÁ-ø’TNÇA𠔨Å�R¨4…óºë®k}éK_*`ÉÀÐË/¿Üú½ßû½r�íuÀ6U3•�ÀUœÉ{ ¼€uWh1Ó–Ÿ^üÀ  ]g  ]N¯—】Ϡ�Gh³óTØ�d/ט¥ÔPýQ®A0€ç·k@ÀtMÚ ˜íáæxøàD«¶ì˜ñô{ñ×øÍ Åpš�ÍŸ—‚^‘=ãä—Åã´…•öøÎ`jœª4[Dà_¨Ù�žRn ,öxö¬Ï>ûì“€¯oííÛ·¯(Í·ß~{kÇŽåè·`[Fy&ýZæ�Ï28------mÚlµZ?mµZç50ü�&÷—jT•?Ü\_Ë÷z#?\“gË*À±5а\ê$Ø8@Ú( (Q�Ø�°h€&—B ”�³ë¾Y*I;G™_ÔMa´B¨sO ÏV<¡4~ä#)J+…Yè1€äòwAˆu¯<Ôð<wíR|ùâ>  vA:X£ S’�� J­5F`î˜q‚8saŒîw\q(�ì^ lµEáîÆ,µe¬€˜:Níˆ�“ –Á4@µh`®,8èOˆ9U]ž.¿-,XD�ï¬oÊ1¥ÚõTfs®}óJÑÏî-¼( ðÉ\ië-/dk[¾·9÷̺²h�²£ö;fÁs¥NS�å›7ãJKKKKKKKKK‘ŶC[ÛTÝ7v.óz«9ßË6…kÙñ ž\{›L) ø „> "€k€‹Š+Ÿ QF‘ã` €%!Ï@L‘)°gË#aÒ=ôP W–G,<�F' *ÜY;ààQRA*8Œýl©  dR?�ÿà?Xk€ èìa,äDJa¼TXÇ"�•‚Ë?0kÌ�š pëZ@ûöRŒ�4�Ö®1v‹�lhsEAu¿ëõKÝ ìåë#@¥ü‚wà*¼™[(i›ï`¼ëWŽ´qkG»àvv¶TcÖ§*Í^</í�tÇ<Kã•¿k¼¡ÒÊoöŒ(Æ€Þ±ßù�ß)×~þóŸ/ê²ùmnÍ…ñòƒOHŒC.2åÞg�?Ænþc+©~,àáY*ÀiiiiiiiëÔnlµZÿvLCûL†?O®­¨�ƒ*0Øj”T�†ä¹*|Di à¬A x)žø‡xŽ(‚`É9Ûê\zé¥E ¶wìÌÌLQõ!4Xµi9¶ ê/ S6ˆ ÝåxÞ¿�cj­|T`ÉÿÓ·œ^ú�ý{Á"hu^81ˆy€Ux¯6´ÉWÊ%H¥~òÛB�eس�`pGUÕ®öù â]Ë/*¸1PIù!ÅÀ•B j½À/¨ä�w�¹¥¢G÷ËŽjÎqàº{÷î⃜_�*¬œŠËÀNU§¾òÅymºÏ\êÏ"„…�‹ Æeë#û‡yÿìg?[”kÊ®eŠ2•ßïž�çmìæ´ëˆχ…÷‚pÇÒÒÒÒÒÒÒÒÒÒFhKcšìT'ÜÞÀQl©Õ䟆LÁ 5Pø-¸¼õÖ[‹ÊIÁK -ª!ƒA!½Î�cà%\WH-à•÷{ôèÑR)š’KA¦n ó¥zíQ«²³ýƒU%à�e¹¦àš: šA™öÀ«ö�„¹–�ö²ÑÆ¢?>F˜ò³Ï>[ÆöÞßþíDZÿ¯qF!'ê¨û¨Ü�4›3þ ©6O±p� )®Tks$4° A6O ŒEs¼�M»€\x@ ´<XæU12 .uYX¸ç°UÀÖ>¨^XX(aËœÍeøSí̓¶¬q5^¾2puÕÈó »OA- ¸Xž¡y¶@À?óyà 7(Fm1Â3¤(+ŽÕj ­OKKKKKKKKK²� €çsë£É¶SŠ`¤€8pæý·¾õ­VO?ýt�"ûú‚J[ê€Lª$ì€EŠ`¥EÊ©ã¡B ƒ]_|q�%0å'µtÛ¶m"…\Ë·¸ ” I=¶÷-P” j©´ Õ5@<Ë“¥¤ò ]~ùå¥o�ª%`Ó&HLvmÉ×ý¥_*ãÙÚ¤”Z°Ö®ë)ÜÀ8¶ûÚ�¦ú ÞRZ©¬æÒym�K x(Ò`Ñý :B°5œ£Gñ+í O±ÞlPo^]#Ìšúnî�9ÓÆ©@YlYeþ�.%߸ø,÷»Öý, �y¡åü2î?þã?.mRÞåy[P°¯³cú0oÚÆr‚U¾¶PЯeôð,C ÓÒÒÒÒÒÒÖ©mmŠ_�Ò@÷ΦàVÚ„ÚŠ� f§b_üâ ´Þxã�­è±½Bp©˜Ô¿›o¾¹�V(ñ­�cÇýî›)§  ”‚<à T)ÈÀDƒ< ü€®vÀ²°^÷¸Ÿò(ï Vp«íØBH(`È_J(€å ¤�:*§~ü¾©¶”TýêƒQf�°|e*,5( âÚ½^xA¤þÌ'н`×O÷ʽå?ŒˆZ,p?¸–›LmýBÇA«v-¸ÆÂƒÇ€¬y3gú�ü˜A8 â`Ö˜(ÈÞ{>æ*æÍ5àó�Â.4Ý|·{n-OÛOP®MóhaãŸøD S§ø‡JÞ¯%�Ï€ÓÒÒÒÒÒÒÖ©àm#ÚE­V뿟r4m¢ì”h€²„Õ•W^Y¶5ÚµkW e<ŠQ,å£R-A �–T\[ã�TaÑ@ׂ$…°@°cŰ€`rŸðg`hÿX JÑÂ,t:ö  `6E—¨ÁòN)º ÝV=ÆA Öï}÷ÝW”TçùÄOý:ø¨• 4 1æ'ØŽs|õ»{"<0‚q°¬O¡Ê±'2¿A$˜57擺 ¸¦Á05V»Ú3^}R`)ºTXs'$9¶|ø`ž*þàƒ–pi`+§˜ÏŠXnçøe,út8¥T+ÆW  Ø®’ÏL¥÷l)ç~áé ˜Ò „�ìæZ‘2�”_}ê˸øë¹»øðVS\----------m„6êè¹,|5vJ, ÌZ ¸€B° n„<Ë-Ý»woâ{î¹§\'W×uŠ0y ‰Hr? Õ�b(�4Õž?õ©O•˜ æ¨È”]¿b¡ËÔC€Ì�(¥5ÔM`ª/*¤MBsåÖRŸ2Ȳ æ�¤û¨ÂÔTÕŒ©¦|ª ´ùV)Ñ Ï=àdRgA-h¦´Ê½5þ /¼°�*05.×Z #gX»­&‡˜yOí¾ü3®ØØ‚Ø7àÒØ-Q6 ¸¹�Ìž5]¸³i*­çcq@x3_,X€~j°kÀ286Ï _Q´Á¼°lçõa\æ�?á‡U—âüéOºÌƒÏ w Hö7ÝtSÉö,DdîoZZZZZZZZÚˆm”y¸ MîoÚØÛB T��Wl‹DщÔW[ ѵ÷+5P‚^ïcŸ^y¸ ”B)† Tmß¾½€8g ŠbDA!س`¸F-oVd÷R5Áð[ ,«x ÀÀ€³”j¾T°¦/аëå8Py´àÐ9÷E…cð¯M0l-ø)¤šÆê>¡ÆÞÌg�î~í PÝÀÚ¨¼üÑž¹0_`œ‚c ùÊ+¯”ðn�jÌ…ؘ@¦pkaׯ.\ÚüD%j×ë“O6„Kn{üR§…®{¶€ŸOBµùéy¸ÏkÇŽ'÷5Ãü’×kÑÂyŸUªù`‘ƒQÙm�¤}s "Àøúµ �žetZZZZZZÚ:¶G0´¹æ•6%ö68ö¾õj5ÛQ)�à– ¦�!(>«z°ó € 9ÖŽPa `)«”Z¹§€ÈG �+X_@ŽZKé¤Þ-aµà�ºŒ?÷ÜsåXäËÕnTQPÍw /£BoÐé§¾´nµï>!½ÚpQÐ ºŒ:&¤Úµ 4;§ç.µ6à–¯Î�aj±k@'€Ô'(¥ÆòQ²9¡‚Ëw>|øpQ»)ÚæÐ\^{íµe¼ëÛn»­äýÊÝu^;r�©¶æÎ½ž�Âcàü‚ .(€ †åmǶTæÆ=üôÍ“¶,p¸Ïy}{¦üþÀ±Ï„ñß~ûíeñÂÂ�Ï 6F*6Ú"†gû&§¥¥¥¥¥¥¥¥¥�І-ÜùŒT~§ÏN �n7 ‚€ÀJ� äP0Á¬­‡�U0òw)¾B‚Áå×¾öµ¢ A”8Ò W I®~@µÐZ° Â„Ü ±¦j‚7*.°Ô†¼Zꤽ:T®eü”תP@“¯ 0)È Îï €D9Î ø{ßû^Q]CI{ÀÒXAª{Ál(ÓTÙ(JEÅu(§ jÏâ¤~R ]ã¨Ù€X¨7ˆ´æZ´°bЪø�ª”f�,çÙ‚�>ùJ ¦Fƒz}˜KsHáÇ€ÝVRÔe�O¡7ŠZÉ‘æ›þÝg<€Ý‹ñƒZ¾Y´pÎ3Ò¯<aÏO•h@x…PƒosÚ�ÝB<¶†JKKKKKKKKK‘ ‚çð=«Õj;ålÚÄÛª� بw”XʨœP Ô€'…Q81Hh ‹ 쀲�çÙÙÙ¢ê‚(ª#ࢨj`¢lmD ^ö�ö r£Š38sÀSØ/ÀRÔÉýöˆ`ÙyÐÙ˜Šss€hO\J*?Pv怚?àÖ8À*%—aáÚè±u‘åÔ|D1ó„©ÒÆcÌÆ�Æ…xSÌ�(Ð–Ç 6m#$8ƒnógñ@û S¨6ÕÜØA¥9Ô®¢bä?/ÿTÙv¿ùá‹m‘ÌuWÆÏ_aÝüô,<G!í”iÅ</ þÃþ°Ì‡íš´É7ŸÇB‰öy¶­p– æGû*RûÌDx}ÄJKKKKKKKK‘�NÓk`u®ãÕ /¶ÝmÍ%øN·múÙÏ~öO­Vë—;GXìÀoTN¦þRdÁ{ì±ÇŠšŽÀH– Šä >E°¨—ÔJf;¥—_~¹�øúË>úÑ�–6À"ä‚gÀ饙rë=p… s}á _(àK¡tüòUh.HÄú J+ÌEm¦ú‚»K.¹¤„w -¤ �r zA¡y�Â|‡ <Â�͉ë@+ð«�•:Ë7ª©q*Å?° ôA*Ð67 ¸R¯UT6F0˹º ˜Š Rù*ߘ2Ëw£-<˜{¹ºò9©î€×3ceÏÐ 7&PöÁ±ù4&`kž„«¿ôÒKeNøKÕ½ì²ËÊÍÅÚOy½ÀÝX„<{ÞÔ`yÀÚÿ?üpé»z#¯¼[k®ÿo›6m:s£ÿcMKKKKKKKKÛ¸–…eëØª 0èlH P£*R……ËÕ› 0'À[÷€I0K�J�‹ò(ìLiJ¦öL—Ú—7 м§dRAàÆ Øb;#°)$<êOÛö¬µ±\ýÉq�, e¶M\Ždª¦ðh} A6À r�ƒlp Š©© ÒÜÄ‶ôë>‹æ  ›K TA³6(®ÆE-UÁ™: 2åW[hâaäà”?Tmïݧ}9¸`™º,äøÞ{ï-ê0ȦÊa*­¶ø§ @ hù¼ù♘Ï�{n¶G2wΛónþ�Õ36®Pç=/�¯ϱÓònZZZZZZZZZZÚ¸ìmU Û ¨€7€H->� Éó¥p^ýõE¤üÙ˸9'<—r l)‚—^ziH¡ºrZ�¤�\m)ù¤rqåøR!U+¦ž …]€Žr €…¤Uªñç>÷¹â'�–r]ùôÀ7 —÷JѾæšk „‚k×E/ï±>¢pÀÔ‰àLó8:¯M¡¾¶Qâ§û@½1u`Iu�|iÊ/X´%%4ÊÙµˆ@í¦*«Å÷�Mê·¶UÚæ³ùç ÖyàÎó" [ÿü°(ÀGãäÚ¦¬ƒw`M¹ÏÌüi ø:n< Tƒ|‹ š��ꮺ7ÿ,D¼JÜÆâ�iF z³ tZZZZZZZZÚF·Ü½£Ž�À`Å 8RðçAà“ë Ü(‹ ðœsÎ)�FbrU�¼á† <Vy¼Ú’Û èœ§» <àç:9±B«ÁðrpÌü¢;J/�¯|£º:ÏWð-™ú©ý]»vصÿ0õZΫBS|§â “lÚ¡:ƒB�hœ�ØuTÞ¨š ô@¦ù2 �i_q)þhP 5T.¬{©»î‰Q ôþûï/°ÈÁ2µÇ¯1Rà Œ�§l Ž[”à3?A±yfüpT =ºæÛïBÃ='c°”ßÐ÷Ô\>éOzlmDi·�A¥¦¢óϼðÝ‚€Ï@„O›+Šõ [µ[pZZZZZZZZÚF·à:¶b4�ž`¸y9µrBA¢ÚP�0¥€@0%5¶}Âu©‰ �b:)¢`Qˆ2P}þùç‹Ê(”p‚c�) ‚8 ú„ÚR Uob”^ªðùçŸ_*Ë;СâRL)½ThÇ„5p¬ï¼óÎ2Î'žx¢À/5Z.-°§FËå•KùŽ|šÔf¡ÖÆÒÍç·9QLÊ\Wã±Â°©Í@^ñu×]W¶jÂÍgóhŽæçç œ E6ÇÔãɦ¼Rsùg^ùP�Íõ±�‘6)ôûöí+ó¤8™¹¤ÀSsÍ¿ûõç¼±šg}{¬-hßâðõìB-ö™�×­]Ь­´´´´´´´´´´´´I³S� yF…ŽvŠ}k¿¢N o¦$3ÊŸp[!¼öõ¥ÆR �%ô�d¡¹î{rQÁ.åp‚éØöǵòYA6ø– .UœÚ �Áe„ƒ-À è�©°b`G…TÀÉ9}‚ô¹¹¹R‹* cOaàé>!Ô€Ô�là®Hð«À�šKAÚÚv�¼]¾ºtNçÀ1E¤Rb©Ò C)v*A1�椚kð+dÛ¸ßxã�â(·ÂÈÁ?¥Ì[h°H`>Ì¥¾øfAAŸ€Ý<Y@pΘÀ±9öÞR`ÏÜšýÕš/¶â38§:닯Ô|ÏØ[°ð¹°��–––––––––––6ivJtXû¶5@h�Gj£ÐV*­œSª <W×Q`) B¤A#e”ª´€¦œ‚N0mK"ûÆ:p¨ª±°_š�St p‚XʧþùUœ�&°¦æ‚f@Ú@£=„Á�Pc ÉOaÕ`ØõÀÄ ‰Õª.ƒ¼‡z¨¼§ S­½¨Ú@„“¾¨°üá[«Ù6J¸7èU�Zml¤_¹Î‹©ËŽQW] .ýåæ‘o®“g ÎùxðàÁbíXh0g@T[”f ”eÀËG£Í§aåŽÅ"‚q r··oß^ü³±•v,|xaÞr½™‚�wa߀<¶¢¢ƒ`¯šÛetZZZZZZZZÚF· �®c+pX„/ƒ2J%ॄzo #a²®^ ਎€à d ˜B Xå–*ž%l™Bª @F9vÐÒõ1ÀÈ9G¥¤°z�?ç@)µøR9µCé¥+xb�¤"Pü§”ÊÃÕ Rßòi�¥öï¹çžÚ ²…/qÍ„»,Ê!Ö¯-˜„\[ P¸KQ®�@�NíVŠÅxbïਠmþÀ½öø <�=@æƒë[M¡*ê´1Z\ † Ó¶°ð×ó×­_ùw¿RæÅyã4.?ؽúº,×Ú³áGŽ)*0%[ˆ;U0 eöÂÌùëáá±�� �IDATeAƒºë™òèó͘´c!ÃxÌ«kjZpZZZZZZZZÚF·à:vJôrFñh”U�¦ü˜ HÁ«P‰)‚Â|ˆ£d ­¥ä¾ùæ›§ª Ó¥ü60GAu­ÜV åhÉ1ÇT\Ê-¥–«}Š3sœSx�¶`™ŠJÑÕ'ÕÓ‡GÛõWUŽ ïù`]°~T»±|¡€‚H¿G,X䣪Ç�ôÊ9ønáÌš]Œ©¸àLÚcW¸4öýïñP^ ¸pp/û'[HปsþPm)è®×ÿû~û}e#·×˜@2?̉çò³?+ó`q�¬>ðÀ幚?Pl ÙB……Š>µ›ÊMɾå–[Zßþö·‹šn᳡–«ú-¯:Tÿv‹�ú´´´´´´´´´´´´qÛš� ^„×R©¥L5bùºÂš)«�ÙO I…FK5PBc)¦ 8jë+_ùJiGnªóBnU‰ �: ¦cÉÂ�2¥ÓO¾Ý¯ R?TTê3PÛ½{w�ãPT)Î@OEê”kŒ ì�G0–A¦0hþ*R^åËø�Þ õ'ÏØO êzpH9�<]@è˜mפ-¹Õ ›Rj<æN?� ƒNŠ,Ð5çÀ M� r)ïŠkQkµåz úr½°o×ËÍ6?àøþþJûÔuó ŠµaÌ¿=›¯¾úê2�ššm ü¤Â;n¬Q`KÑ+…ÎÌ H·¸Ñ©þFUñ´´´´´´´´´´´´‰à0�!Æà‰ŠzèС¿�Ìñ|àŽ�0XuN¨- £2úI�tÌy÷P“Á%Y-øQ˜]sÇwµ<Ê“®+|8 ¦â‚µÈ!¦b‚@0©��Æ Î@-P  …&ƒÙG}´T{¦4S;AulEÄ� :µ­Ø“s ñµ×^+Ç©Æ|ÅöB¿Bƒ�WæÃT‘6>c…±  TYØ3 ½ýöÛ˘´ÉoÇø…²(í@×9íPtµ¥F¡5B­-.xQwÝG1æ;Ì¡bUÆAÍå‘O ÒÁ/ušjon�4þ,gð�°pm¸F(´6Zm9äiiiiiiiiiiii“bkæ�·Ã À¡RCRŠš(?tÊG¬Bw)‹Ìõ`H*†Eí² “*«Hà{æ™gJèð#�<R€0RÀEÑ2~ÿÎ�túxÉ{éŽiS!(J/¸jZý<j¦‚\@P®®±W¹Ä �ê_¬2[ȳ{×à4V`T-�˜ É_°ÏÀ$竱kË5üîlÌ�ˆšOà ‚]çh§�›o¯É67@ xÏÀX,€bý©:mlü¶ÆnñÁSŸ….[�¯ Ž�îoü§ß(cJ®]njǽ®q-8ÖÏÌÌÌÉ<kÊ»kŒ§gpZZZZZZZZÚF·Ì®c])À ðX`àB%¥Fª0UP4Œðb÷ å§Q‘Ùq@ª€�Ðc�% T:Ûñ€4a» Y›üˆ=�åÛÚnI(5¸ t�+uU¾®mz(·Þú¥ŠR}…ñª¬p Ø(¥T\�¬o@©_c§ ‘6vþ§±�fcw­ðjý€ÐP��¨¼dc⓾˜ógm[ p/¸‚ $ù¢8Ö†ß)Í ØR]Á/@×§¹¶ˆ 3åb›O³õ@ûú~þóŸ/Õ²m[¥?0 †U™þÉ�Rú£êÛŠJÞ°¹ún±C(¸„ŠhrÈ�Ãü¦¥¥¥¥¥¥¥¥¥¥¥MšuÀNr…õ‚`p@cÛp 樒·K=›€ DíP|cÏZï�$�³­�üY@K±µÀÚõ/¿üri d a�ŽóCßòkA/ÜQhÁ�ºï¾ûНÔJ@Êø$—pÚÆ‡2à � °Ô&…;ëL �zü�@™êL¡å³={�8UZá*~‚h  (xê6¯9&Îo~ê0W0n¡Á< ov�=€…ckGØ4)ïB•A¿1€s…ªÌuVXt(Ô~>öØc­§Ÿ~ºTð¶x@Ñ�yúæÆ8½ŒKñ.ãà �÷0�®c1.`¿œyæY+----------m\ÖS4¥S.«]…£À(e´^Ðæ(«@S*0å¼�Y ÜäúF81`t*ƒ`×iX]ç€àâP¥TjG»€W˜1¨ó¨‚F@.D—J ïºë®Ò¯6TC2ìxl±Äg ªÛ3�9ùÊrl©©ÔO*²>„Û&Œºd:ïœqØò  m6戊Ké�úÍ|ÔæÈ‚1›_c£¼º›#ù¿æÜ i>mž@³… îS½Úõwß}wTê­f Æo~�¬9´…“<`Ç)ÁÚÜ�?å/{nb[?)|Fµà ŸÏܧ_ã S�Ûa7ªA÷S+C ÓÒÒÒÒÒÒÒÒ6ºetë:š ƒ›À(rp©œÔE*¡ªÃ@ˆ�J×xú�/£j»ÈšÂhU†bÂ�$À¤˜ qÚw¿Ü^ EÁô„ó�º¨maÙÔKmÚÃvÏž=EM¬/Àúëÿá× {Oq– ^ 8p¶–að*´;ö½÷ú5Nê)È h¤|�Žïüùwл Z»À\1è5>p¬=s(A3ؽùæ›Ëµ`L›K÷™K°J½6Çæ�esD¥W!š¿Ú±m’93Ú³ à=€µµTl!e‹#íóã?øA¹O•g}¨ôì™Ù6IÿŒÙ|É�6/­FV ·: aXgq¬´´´´´´´´´´´qY×U 8l”] É€™ÐÚ÷¼ç=Œ…ÑÎ(Œˆ@¥“: ´9½�ÙÖ;ÀÚ1÷ÈkÒ Á$Ȥ@QШÏ�;w–"WªCƒ[pe¡ÓI0M=¦DR.ù§ *[ß|t P…r_%˜õ“¯Šr}ÀG ®T^~ÚûÖØŒW!­�z ÊOüìÄÉ*Ô€”Q•)· yÙRˆŠ¬m}Ù*Jq*0ï`,ìZ¿TX‹æ…J,ÜšânîÏ26!ÕQ½ú¹çž+9Õ¨ÚnU ©ã”v½ÿþ³ŠXéO>´gk¡Áƒù§8˹¦¼÷�~ô£Ò¦íŸÌ¹ç¢MsäÙ˜KáÒËYÂoZZZZZZZZZZÚ8­k�¦ƒ=p¦(‚ œ�[v…Ö‚=€àÀ(µ”R8)› ø)¤-÷€>¹ª  ä�6*,`lT``²"´<Ûó— 4Í’? nwíÚUB|Á •ç PcŸ\aÄ~‚<à,WW!-&–úkb¡ÀÆèÿA¦þ'?©ÐÚ¦­Rµ¾Í‡>…eW«©†m,ŠTñ pÂØ €Rˆ….ƒ]!ß”_ 0ÈÔŽ\h@O‰WàJû@<»Ÿª ŠcOáØ7Y.𫯾Z*=›*±y�+¬?E°�®í’ÌUX~±90>àkøîùØ6Éb…ç+ü[žuD È%ö¼#ä9-----------mR¬ë"X¡Þ�F òb‹#…®ÀšÜP!¿Ž�<�¾�*%h�_*"x¢(S?AUly/äÜ ¥U¡Ùu@T¨­SÚÐ?˜£ìK Ò�±>ùæÞ­[·X¯®£ŽRMùÛéÈ�þùÂ]ãy¸^Oà p)»@äS>-�=c†L½¦ªªvM�å+xM�èàXß~*´¥ óË?í«B PÁ¯y†­ʯ…¾WŽÌ=_Ìõ™ŽÉÕ½à‚ ›#гû]ÃG B¥�5ÀÖ¯m¡�³°qÏCQ-ón<æO¾¯ÐqʵÜac|衇 X»Þ<ø|¬d Åiiiiiiiiiiiã´®Š`µƒ £~Ðä'ˆ£hRa´±%�0iðî„GS'åø)Õ¢�¨ã€ì =® F]go` P)‹îÖk¯^�¤A(¨Œ-‰äÑny¬ CÉïÁr‚ùí<¸§îÕ�.ªRH� ú(ÚÔU�ì<Èw¼R‚Á²6@*�·�“P`ùή37TTê7@¥ØÊo‘Ôjó¦ yÅTeý Gü^�Øœ Ó&Îy·ò£�˼ —óÌ' n�ÏóxßÖ÷µ~ðÆÊ\¹Ç\šw>*†å~㤰 ¯6ª;h6—@4[Р>[˜Ð6¨7~‹|Ô&¿øg>W‚Ü~ `µ²VZZZZZZZZZZÁªd])ÀÀ ̵à (mBœA±ít¨¦òX�,€¢&†² î@§0Z°~�-À¢D‚Uy½`Ë^³ŸýìgKX®p]�Ψ‘rR対þúëÔl?$dXß@˜J«= +˜_|§SRÁéo½û·Š: š)¥ü•» �A"%š{ã* µ°°PB{åS€��¯ϱ1¸47 Sx3?„&GˆµkùséZ0j®„}SÉõi‘ÀqÊñÜÜ\ ýï”osþͳà@¦0ƒl �—^zië;YBÑ©·Âµ�Aåk `Ío½õÖÀŸþô§ ¼R޵!ÔZø³g$ÜÜ¥ßsÙW]uU#¿øa‘"s}ÓÒÒÒÒÒÒÒÒÒÒ&ÑÖàk턚PC‘PgŠ+P»ÎBÐŽo»í¶¢|zïœ]À °(¡`ObŠh¸aÔ^mÛ.éÀ�Ea¤*{ÍÎÎP•LáUô) ìñí;ßùNQ$Áµþ…î‚Æ­gl-Š%X> ¦m¨«À3|ŒÂ{ßõïßUr…co`ðKAÕHÇ Èò)rpµ-ìøúë¯/U”A¨ü^Ê+ Ä •âm.)ºŽ}ÿûß/ûû[×·škJ²þÀ¦¶åíš+ »mÛ¶õÌ»}~�.?À®yQ¹Ú8-P‡ëä:[<ЖÜh*¼ùÕÎ?øÁ’§í:¯}µ/:PŠ#2À3NKKKKKKKKKKK›J�¾ºí¡­TAêo«Q1©®¶2�T[ .Xz,l¼i06A"øt\5bm>çå7Ê¢0kŬ@0ÕXÞ)µQ®¥t‚DJ$h£nR+õúø…¢À|Srå÷‚;aÊTU¡ÉÔf�îz€GevÿO~ü“ØÂ•ý¤€J…µs/ÐmÜ Zq,…Ã�-à^¼cÇŽrßmCä¸ÊÉŠ¦ö@¸UTh‹æœF^²v…ƒz ËW*.�¦ŽS´…� “ð\ŒÙ³ŠmÀœRx-:»ðjê1Åœº ò͇œd°«O…®b{#‹ æHló šòë™ð%-----------m­§m�ÂÀªP_PÌbZâ@S�ŠÚ(”š ”�õÒ¶A@5Š-¹t�+Š%�z …ˉ’¬p”öA¢°fÀ ©¤�T{ R2a®wŸ0f€FÕ‚MµôÞB‘à L…DÛB‰½ñÆeœöÐ� ?Q>�9Ðç¿>„ëßýŽñÛ–JàÐxA©ë]¼*õ„Z T»�Bm<ò¦Í¿Œ„‚`¾ƒzpmQÐ[Ð6u˜’{øðáÓàVHµJÛ>ü]oQ‚ olæ‚’Í×{FÀ×ïæ�³9¡ÂS¶)ÙÔ^}‚w Ôxsž–––––––––––6©Ö�·j�ìÊ#½á†JXnqÃ�S5dÛëPL�–`À*Oر½{÷–ã©Á‚{Ýuר“C à„FS¦]§ ±‚P `ª¨ìx…1óIÿkùÂ€Žª+´Ù¿Bx]�©¸Ôe~SK,ØÚ�î|þÚ–æ(äȦ˭ r…*SW…ˆSc)Â÷ßQµ…:ëKȸ{\ ˆ¨-£ŒQ $ƒOyÇ üñÚÔn‹Úª ‹Q²Át€´çA%6Vs Ê�¬»Þ~ÊÆåZ÷ڻ, hŸšL]§´§E�V“¬M‹üKKKKKKKKKKKK[w�ÜnÀ À [º>ô` ñnTDJ¡êÄ ”(`KE™”UàÖ„ü] 0@¤özPýð§‚jS¾*(¤DîÛ·¯ø *2ÐÕ–<XýñÐ�5á×gŸ}vÉW”Pê(¥Ó1Š4?µ)Ì|L@Ky6ÍwЧŸÚ¬ [¸¶hûô‚G éu˜êËg¿[Pì 4x~�ac“GKñ•ü6nN ù Ú5Š[éËü;`)´TlÊ´yVŒ-PØÞˆJ.œ™æB10ª»jÔž‰vm±¤í{÷;°—g ô)ÁÆm~©áBÖ½çoZZZZZZZZZZZÚ¤ZWÛ ­eWà%ÜWî)P¤,R)œBdÁ•l=á» ?VU’åÙReÁ–ŸÀ,òz�ð}õÕWK(´"PB£A¡v�5ÕHR“A›m“´í§öœ—/ RÁ:åRuä ƒ7¹¬�—}ÅWÖ/•ÈRN)Örb]ÇÊóüÁEØø�,•7�îßþíßµXEå /¼°€£po>„ò xÁ68maÀ6F|ã·û)±¶"¢œƒéØNI83Õ¤ ¯6ÿàÖ<«Žçá9¹&ÂÈ£P™kÏ $óß¼x ,\yå•¥J´¹î|Ó/èÆÆ¨0™>…i×¶Ü)--------m£[nƒTÇ�ŽêÐÂz¯¹æš› !�"?‘NB•Á'ˆ¥˜R&Á–\`0h…íRc­û…øjÇ}a˜)¸t òCúÐÉ ËÂwÁ³\d@©Ч= ©£ÞC[1�[P¨¿ô¥/•ÂTÔ`×P8…Aƒxê'ðõ“ Ž#×SQ©Ý`[(³ñD€ŠåÚò™’ë„�GŸª?« -¼0S�ù—~¹Ì/Á)-�PJ.â‰'N*ÂæÎ uÎödæwôsðàÁú žJ Þ-F˜_ã2�mìB¾[M�3sHá¦üã/j/e𫱛+ðî÷VGˆ|ZZZZZZZZZZZÚ$Xß 009^~¶ .íÚµ«(´”G¹» 8 EZ A¨œTJ#`�”Z ç>Å©À,X£†ªž¬�Meµí‘½q©“� �V*¦ªÐªE`á׎Ëù¥â‚4�J­°Â‰)¡àW»îßÔYŠ®P`Ê,e˜o``j#Œ* Jå&ƒrà©à”|Ù›nº©�¢°jíTaÚÀTEeþ y6Nc3/r�…‘ƒn÷‚fùÏB�©ÀÀXH³~°’Wmøà>*»…ʶç¢[QÇA«yp¯y27BÍUs–WìpOÍvNÙ6¿æ‡Z Œ�ß32Çúá£ñƒñ{îØ:«ÆžÀ©�§¥¥¥¥¥¥¥¥mtK¸Ž  n€ØQJN+¸¤„!�çA�YàK™Q�V&Š&õ5¶NLqª™™™R¡”R€&ä ªt â)½rx�¡"U^`Œ—=|ßõ®¢¸R9/ðsLè/…|º ‚@…¼€¨^� ƒz°*„[¿TPyÈÔ]çÀ-V[ætRK�$õÔ¼X,àƒm‡"�˜B+ÄY­iã»”c�N•¥Ôêè›W0­V§Á¶3zôÑGË‚¾ÌpÒæÀïwÜqG¯¶<ª³>½¾û4óÝ}ÒmœóŒü47®ÿÇÿõ�EÍ7'L?Æè9Ö‚ßVpZZZZZZZZZZp%€ ¥ 6`IEM–'KQ½Âƒ/¿üò’ €‚'*£8ÏBr0 -LZUh÷€/!ÕB…… ‡Ö®ÐkÀF¥lÚµª(S2Ùr=å’¯ ‘*-ŒX»ŽQ2åÓ 9¦{:6Ÿ(È®§ÚË@Px1åzª­6íÔñmÛ¶•ãÀ_¸¶ñ#5Y8³1€v¹Ñ|pHƒuþ ÅœBÆÍÕ§>õ©¢ìÞvÛmÅOj,µ\»ÆF%Ù�œÅ>Ì”d ¨ãÚ4&ýk<ùÆçœsN�dÏÔÂÀå�þ<oÀŒùJÍןûÛ?ƒZpZZZZZZZZÚF·à:vJp¯Ð¢€e—ä€!À}ñÅKî­pfPDbç�w^[ª°<Y ¨Ði!ДQJquÏŠ-�+p& €RƒÁž¾ÀÓ.øt^ñ-°Gav¡FRIù ¼­<Wǵc hQ<)¾~‚CJmT–o Š©²Ô_Àg|ì[ßúVi“ÂÕ¡U¨öÞ¸¨ªÔYmgŰ(¯”Ö�;wÈçúÎì~  „©¶ò“mÙdNù¢À—>Á0?(Ç«¹}“Í™ðg  �4ØOó#ŒÙØù"Dݳ3¿ÂÈ�Ã\©¼MݦäSÐù–݈A>Ó7sÎ"@X·yÀ™/œ––––––––––6 ; À`ˆÂ°º…`àBý €01pFuU¬ 4 ›¥ü*òåpQ`U[gÎëSÈ1`ÅÔIÐ �(”ëª`(_¨Q}A*-”¨j ò�<0I�pÂŒÁõ}÷ÝWàÓ8œÎ�’ÒÌG÷€W猌RQª�¿ûùÒ&€Ì”k¹¿ªTSˆå?kï…_(ê)ÜÔjÅ£Œ�Jløk¼B²¿¢ Sd�$ó�ŠK¥ÊlŒî³íÕ]ž¯öåûË6gÀ[þ¯Prc®,¤Ús7Ö;v´n¹å–m�B8ºbdB¯=_ ?È5ÀÛ=Îñ“â­­Ú´´´´´´´´´´´I´“!ÐÀ�0”½nÌ}` H�£(Š%t¼‚)à(ÄÄ}ùË_n]yÅ•e‹¦b1À´o®_@ à€¥}x©‹ �*LÝ*L%p”dç�4ÈtÍ¡C‡Ê}€R8´±hÃ5î¡p‚6Ť(Ò`Ò9Š'••Ú Âåïª(í�ê0õL U¬_nË&y¶ `‚VáÄ 1öÙ½ñÆK,Ê0Öqð(4Û|øÃ>¹ ÀŒ­Œô!TÛqpVõ£úµóî¡Þ:g¡�²­PŠõe>©æÆ£m A{(Èž›û£°•{,ZXhRí«ùÒ/½�Ûç†2Ýü®µà’!ÐiiiiiiiiiÝ2ºŽ½-˜Jò„²‚Ún hQeCid`�Âê85VÑ&à8??_¶<b 5�;ÀKõ»œYaÊÔܧŸ~º€…“9g‹ 9Ç>�€�Ú*4˜ò ´Uv–k ‚)«`�Ê ä€õw÷îÝ¥°u4 ïìÈ‘#%·” /ÖŽq¨Í€!ð3NyÇÚ§ÂR>£(XTuVš «Š4ÐÖ¨”Kž ø�º\^}SÏ�¹qšû ƒnyÄúš"€jÏ|/h".ÜÚ3¤Þ2‹TZp.äÙ\È:n‘Bˆ3`7WÆe�DS{Í¿÷Ä"‰E >/”á•à7*…·È N�NKKKKKKKKÛè–�\ÇN)‚lÚó8»µ€¿Ns\›À̶Bà”Q-m­ð(‘”IùºTMj*À“ÃJùŒ|WÐ Îï¼óÎ¢ì ±–‹j�`?A³UX?àP(5åÒ1ꯜW°&D8Tiê)”ò ”m¹D‘Ü š/ OA-×™}ƒÅØþƒq0:=ȵ`˜bl[$À ¨A«übàé½q‚XóÄ'¡ÔÚà“9TÜ d{¯_¡Î”c¹Ö �ò‰-8ð Ü{Y@�¾!ø f/åȃó={ö”1šðL=§&³*Öú—;!à¡S°�™?ËÁo«àn*B›£°Îk€ÓÒÒÒÒÒÒÒÒ6º%�×±·+ð�˜«÷°‘Kª¢0…‘‚Èž¢{�_j%E¸Æ²�’ê©j2X“#{æ™gˆ_`æZPF±¤"S+…JƒB°êœkbë#ŠsøÀ§ýû÷—bV΃Gj5`Ñ‹,ØX¨Ò`ÔSbA¡"Vü˜�Yõeï�¦¶Á¦ŸÞ»Æø�;åZŸÀÜS§Á§…½(ë°õ) ÄZs¥Š4¥ØØ„mcÐ ª“—lLÚ5` ˜…aSm)ÊæÊ8Ù=÷ÜS*w›'í™aâ~º×<�YÐ+dZ€�?Ƴ’¹¿›Ï“¶Á¿°ùZŸ¿´´´´´´´´´´´´v{[¬VŸyWP*U. u™ ëw0X�7¨ –ã ¬b_ÚË.»¬(š�Œz 0�°P]mß{ïÿkïÞBä®¶<ŽWwŸL+Øâ…œ‡8‚WðàQQ1DT0ï‰ÆÄ{£ñÞÅ»ñnT4˜x7Š—1ú }mA_ßäL+œ8*ô舠2|6½2EO'š±Îá~ Šª®ÿÿ¿÷ÚkW ßý[{í;[J®tbpF­µJBýÕÀPÏ›7¯A!”¢¬*3eX*4h¬ýÏ¥œw)Á ìp~Pš�ªö� %Õ@+5<J›–ú¬ ¥Ö¾b�ª°øÖ7•ÚwÔ[G;Õ^eàË Ï'c>øàƒÛ�¢½ÔæJQ+1ÖŽã¢jŸ¯ôi*³Ef¿2uÈŠ�>©½ c{í¯¸‹¿}ÄLü�t©¸æ »·~/Ó)ÿ[²é~_Ú5> ±X,‹Åb±X,ö�²Á©é©ÝŸ§ƒ•m1ÏK�eµ´{O( Ó’>ƒÌO>ù¤�,ÐR �ƒcà ¶–.]ÚžWŠšk/k}¤Š1°¥zv&AVj5?´ô:“Ç7ÕW©Ö€8Ûÿ &]ôÈ–×�� �IDAT£8ólK¦FÝ5kÖ4¸µOLó Gª°teþèÃOüUÁZA* /¸”vmŸîÂ… [0àlo3P§0+æ%ârÝ ¸U“¶ °dÉ’¦rƒ]ŸÅ¼¾üòË�E‹µñw  È)ÍT^±rõY[ëÖ­k©àÒÐ}–Ò,ýºæß˜Àz÷I)ÂÒ¦{e@ÛÄ¡W¿¿X,‹Åb±X,ë¶A)§à£8 Š+}uk/ÐIÉ”zëoiÇÔ¼&ïÓ�L©†` ØÕgàéü_J+0=÷ÜsÛ¹ÂÎ ¦zJïU$ ô�cʦ½±Àt*&X�ëÑGmJ°ôcß9�õG!¥ºVêmí�¥¿ù曭´=¸�W|n¼ñƇR®©ªÚ¡F?À¤ëª%ƒdP©à”c›ª˜@¥Ê‚Ù¿ýÇßZ¿|ÅÆK…¢ ei˸(À|ð,Ÿ(Ëžsn2À¦^ƒq{‰Å´ÛoüÐCµ…×,�ó_|±ù­`˜x‡û]ÚU}Zªu¥¡`¾gÍcíëÝ–Bie[Ê0fYÒ¯c±X,‹Åb±XìaC‹/ýþûï‡^�àðIMöðu&:¯RN©“à QO¥"ƒHŠªwJÞï$ ¾ô÷ß?þØ 3@êh )ºöáX©ÎŠT8Šj¥ãò‡’ à¤ÒR”£c~¤ K³æÀÕ•Ö»}´îÃRª°‹ª=eßsg2m›J\çK­–ì{ýRf/ÀOýê£Òº)»ú—½éïÿ› ­±äX!-j-0ö™‚íš>Ì�JׯcìµgW›€ì›3>�Ûª^íø%ÏSw©Ï€öÌ3ÏlE®(Ä*MkSºµ1‹ŸÏÆÎ7÷oËñX[²é�ØwÀŸïq11§~[“°�"X±X,‹Åb±¾¶Áê� ]qÅ£ï½÷Þ0@ÀNJ2Ø Ô\{[€]®�e ªÀR�Ë«b³tcÀW°¬Í­U.µèjL{÷¬vùAAGúz”VSM©¨Tap*å˜b `)³|š”×û¥Ù‚l×Áz¡$UXA(Jñ‰'žØ”Uc ŽòÍXA` {uµ6ùHõÃö Si©­úp/ÈT€Jìô'=™¯Ôr°î]A*)Ð šUD‹bËw¾*œUç óiÕªU­ª´=ÈÔoÊ2`´yÇ"©0mÁœðWßâíz åšù§úZˆÐ>Ð6ŸLª·ù6×�-@ìoYD«jÖæ¡»-é×ü£l+Ð%Ž|ŸÌ �Çb±X,‹ÅúÚÀ½±¡Ûn»mtÖ¬YÃö¥MJ*è ¾ÖÙ²TZ—)‘ªƒ{J©tÀÌßÀKš.…4– ~©šµÇsjZu§ €]IÞA! �ÀO_µ—o|½îºëZš¯ŠÈ`W?@Ô½TLp¬= *¥øJµµÇ´mÀ§°õÎ;ï4s “6®ºë®»šâª]©Ì|UIZ,@²4íDʱqR~©Ãžòbhý­B´8Y  ¾íq¦T+R´Ånîܹ-}[|ùÄ·sÎ9§õ%Æ”aéáblœöóÁ~dmÎøj«¸€×6�•F]éÍú°Pà‹µO[ÛžçHîlCUç²îyöìòåË[ŸTèN�‹‡¹à“w)ç¾S¸ŒÜétÀ±X,‹Åb±¾µ�polhÅŠ£»ì²Ëp�,…PJ.±ç–çÜ]ï�K¥â*ÖDµ¤®2ÇQ<Ý¥O—j @�!¨Ò.(­³jK t¸g”Tï�•½úê«íºý¾à‰B-�°�Ypê;�I)Ö'E·ŠX9ÏW:1øˆÚ¢Æ‚äJA–VMá£Æ@±µ(�¾µçºØ(N%]Ø=ž·�„RïŽ+òœþ¨ÔT\Ç »ï¥K�Ï‚rc�ž±ŽŠ¢¾ÚÃL•[ª¬¶�+àÓN8¡��¬è—Eª®1kOŒ-ˆ·Å ¯xóدkb¬?>�O×Â…X3ßysÆ?êµþ»«?wïó®ïÁ­qè·²�ŒGl�Åß~Gü4~7�]Ì�ÍâÂgœ¡©�p,‹Åb±X¬¯-�Üû“ãv(—`DUa)±Ò˜©•À… N¨šÒSí5U¤Š *HQ_©¥ 99Z �0°¤%—" òJñ_”M)¾�¬Ô_€T)׌" vºvU6fúb@p9ÖHñ*°ìy°¬o`”)ÙÆ fù#•™‚+]ZÛÔ]`Çm(²:Å�芋âRRÂ=k/-ðW5Z»Æ ÞŒ“i‡Š¬ æØ¤‚DŠsí%v?õÙßÚÑ�g)í Hû^Z5}¦ŠŠè–­oƒR ª(™´iðÏW±-¸¥F›J8ß,b�çJë¶Ÿ¸æÕØÍ™k¥ÎVj|íïî®mþêÌe÷Ô¹Ò5߯æõï÷EUvýúë¯osdð믿¾¹‚5%>‹Åb±X,‹ÅzeC_}õUS€íÍÛ¼Ÿ”jiO)@¡°‚!ʯë®`ÀÆÀ ³xÁ+€¡²)`©€²à­Ž;L*1Sg¥ Sa)È té—ÀÐuJ¢4f¾è”NЍ>¤dS=S•„ï¹çž¦&ƒ/° 0•ÂL9ÀÀÍsdê.eTò‘* ÎÜO­Òî¥PJ_fÔMm�Eï œÊ)‹¥ªò[ÄÓ^]@©Ê3Vj7Å [˜0V©Âž¡@‹�±jð;÷×"‚>A£ï�#%ÝX@®>}Oõ6_ Y;ž©4g±7N±)ÿÅ» ™™sо6Œ½b ¬Í•kü`µ·—\ÇaiÏ;ø]±bESý<¿ÓÎ;7µ\ì¼…c÷»Œ‹Åb±X,ëk‹Ü˜;wîÄÅ_<h€ØdÒd,¡‚EÐF1)î£P2ší;šÀ‘½òÊ+›÷õRbÁÓI'�Ô™3gN2€FYŠTf{dÝ/UW*1àsúª=°`‰ Ë�»=¢� ÀQ¦_zé¥ÍE¸€¹=³ ’ÿ Üý€p9&ˆªÍ��ñÔÞc nœ`�B ܨ½”h� V�9(å ä?húbfÌuN.Ȩ¥€Šà,<;oÞ¼¦ÎZ  dÛ{|ï½÷6 ´� µYJðý÷ßßúPØŠÊÍ?¾Sˆ)ôbô¨9Ÿâ§íS„ÅØ˜,@ð_:²9á¿yížk âSiÝ�IuÛ~lïæÛïÂ<{J·±i�U5ç‚bïæ[D¸ì²ËZj:ð/%Þþß·ß~»¥Dóm»í¶ët:‡ÿ³þ#‹Åb±X,‹Åþ¨ý ÑÆþ¯ ,^¼xbæÌ™#µ—”zXû=ÁM¥ãR#)�€�J*õ0»–@'Fà0�H�ë9p\–À£ªÍW]uÕæâJ¬ŽÛ¡`H‚ìªNìß9ÚœJñÕ·k Ø=×U|X@Ðð­½§ŽPÆÚPŒ Ü4÷ñXj·3©`J1¦FsÊ%�¯jËþö®RXÅ¥ŽQ2vð ,A¬ñ–ºJåVüН¾† uÍš5�k¯½¶Ux¦†\{“Á1ZŒíÏ0­í?ü°µ/âÅ7Ÿ)äÒÀKÉÖ¯1R˜Í òÀ´æHûl!ø›‹Ë/¿¼»±Š½óì÷¢_ãwŸöÍu^|ë8%fþͯ˜b_ê3Ó§E g:ûÝ<÷Üsma`þüùàX,‹Åb±X_[�¸76pÞyçMì±Ç#ÀØÖ™´À�GÉ’TEj õ¢´ºFµ=ö—JÝLRª°©ÝJ¡cRm=ëŠ*e•cŠ-P£:Ru”vÜ JA¦>µE9Öîƒ>Ø|—ú¢Œ‚7€ ��—>ìq_þvÎ._J©”V Á®ç¥NƒS �Ò€µåTÁXãCy’® ú+EYLŒÅ½k×®mÏ‚b@Ëðh1Á÷ €Z„�ã”[c•ž ÞÅA:3ø5žöö^óW»@ÔØÄ ¤[ 0^×ÅI€ª{À²1ZÔ &kËý�ßB�ËLÕݸqcS�Á6èW™Z¬ü,4ø^*80¶À—J§ÀÚÀ?j¶gµK-– PE²,ªH¯×ÆêÕ«›:,¾{î¹çØŒ3À±X,‹Åb±¾µ�pol`åÊ•CCC#”G >�…H�ð p¨¯TRpApÁÈ’® �¨©À–R Œ�ERÛàŽ B/ ßÀ‚N Ô¤1q° �Áe�á«-*!•Üñ]a,j& £�ú‘P�=eÄë÷üóÏofO³q7éÓÆ,]Ù˜ù¥opl)¦Ž/â'À«ý¬àÐßâB%ÖóÙ]¯ôfÀ.-lR~A¨~ßxã�öÌ¢E‹‹ƒt`q¦*« Ø©ÉÔVq·Ø F ß÷{~©]pÿÞ{ï5�CüìïÕ·…>óE Ajíi6� (hýƒ]‹Šœ™ó#�™žÕžTxñµæÙ~p¯ÌYØâáä÷ÁÇ“O>¹íËý®wÛä‘LcƒƒƒàX,‹Åb±XßZ�¸76pÖYgM|÷Ýw# HIs¥f JI•~ L¤¸3° ^ªð%Tè#Ž8¢MŒç}¦ ƒgP ‚±ÒØQp !)³ÔEj£þR(ÁÏ[o½Õ@ °·n +e@ñ‡~¸Á«}­•š«Oª40’€<‚BÀÆiÆîå¬ú „û›B *íáóâB±¤Þò‘¯@×5J�¥ÔZ< èÚ›ká@,�Ä'¾J7– €a)©àUÌÁ*ŸÁ°P¬Á¥øŠí²eËšßæÌ˜ÅH5i*¶6Áú©§žÚÀU ì±5Ÿ Óâ„ù²�a|â B'÷Û6°Æ@Õø|®±4oæËaq3FíÓâ–ÍŸç}oDŒÙ\Š­ö¥;û�P»ï¾ûîÍéÐS,)бX,‹Åb±¾¶�polh§�výé§Ÿ†� Åd‚?p¤•Tb8�*’JKU­êà ,h % ¡ô%�l­thJ e¤(ÏR.)«@ ‚X�«/ c�‡ È´§"u�C«p{úé§[¥g 'ð¢$s ¦ç€ Ф�ìý÷ßoů@*ˆ¢>Ÿ}öÙ š�!ÐsÌ(¤äêË¢�ˆ”ž,àäSÅù,ž—ž\gƒyþˆÿŒ�UJ·…m«˜M•,†àìò[,$EÜ‚TiÂÔv튿1ZP�Ð"�:�¥‚óËØõe—3�ù-þK—.m)îú5·b ÈÁjõ£]ïü¥ž§¿¥1S})ñâã;ïæÉo¡*zKãßæ–ZÌ—uëÖµ8XÀð›0û²A{‰å¹×Æ�Ç÷Úk¯T�ŽÅb±X,‹õ­¥ tol`áÂ…ûí·ßH8’^\ >€ h+UP%g l(º¾§HR6� p¦bTÐFMõ F Úg*çüùó›:è\\÷ØóI–Ž ¼©³U„ ¬�h øìbUØ +ÔD©£`[ú³´hm‚P@G•—ü�eÆŠ©˜T`  ÒüÀ(°·ÜrK8iáÚ5 *&àxŠE}r�êj<ú¦¸êS<A7X|ŠvYpPäiîܹíš1‹…õ‰'žè,\¸°Á<¿«ØåÀƒñó ?Ð[ªŠ2è™(Œ—/ÆG]ÖÐ5ßâ@ÅÖ/?/ºè¢¶  Ð ÌkáÃÅÂ5þòÛbƒ¹áË 7ÜÐ|q¬³¥-X\3/~òÏØøî^s͇K.¹¤ÅÕ∞9sæØÒ¥K£�Çb±X,‹ÅúÖ¢�÷Ɔ?üðÑ}öÙgXú-�¢ðQ$)£À� b¤Ç:®ìJ%BÔR`úÀ•{õ×g0,}iÓw&<Qoo¾ùæ|à‹âH9u/¨ÕwU¿Re¥ü‚3¾�.ðå3àÝ”EÏQ˜A¦´d@Á¶vÀÞM7ÝÔàœÏ®-Y²¤¥æòC{ƨ_ê5ˆÒ®»þ=^�9%ÕØÀÕ=ú¤+EÍ– NA¥¶¤JBñ´'ؙʀ›ò.^àÔþ\ +¾ÓŸØ‹,Þ öñÇoð ÜÅŠúl®|gA0§E~XÌ�š|�ûꫯ¶ë Yú´¸2Š´{µå^Ê2¥ž?€öµ×^Û¼ÿÚ¼)øeîÍ�…ê0(åžÛ~ÀXœA®ùà§Ô>r*¼û¥©‹­XuÔQãûî»oàX,‹Åb±XßZàÞØÐ¡‡:zÚi§ k�XgÞ4P~(ˆ�¸Š=�$ �AfWòÜX{\)¸ÔCÊ!0¤¤IE’¨��d�JjUÔ>Y)¹€Èwúq�ú xí-å…“ªÉNÙÔ�Uª»¦�*Z~©Ž@(S-Á®k ÐøŸqP ùFÉ•ú€à×÷ �O` p[<P5Y»`´Ö1P€U,µ¡=)áŠlQw �í­·ÞÚ|à€)ìR´ùÄ7ýYX�èbcõ�8[�/q@).�¦y•`T‹Ó•W^Ùú‘.í{ âOõ5ï�Õ³@_ß~ü¶o×ÂE— ÛëJó6? $3×|¦êòGü-«ùóÛQd«ÒÃ]ëçŸ~üÒK/ �Çb±X,‹ÅúÖÀ½±¡sÎ9gô�C§�ÄP©ƒ`„Pì@ ÊʠȀ,Ï�9ðì€0E×³à °©ÕÒP)¯”UiÉú�9àK±,ð\Çþ�@�H¥k•š î€1x]¹reXþ€ÌR‡í�¥0ÃÚ¯Ìo>ò ¼ƒ0}Uñ/×ümï.e”Š ´Ý«¿'Ÿ|²)™�� ®{xÁ"ÅRúX¿~}STÁ·ï�&_À°‚UÚ“ö ¥jk4ºß8žy晦±¢–‚mÏW…l0.Î`›¢L•ÎRˆÍP5>óÒ™¬¦ 4ÅŠ²M±õ�BbÆê~>|q ¾xWð‹ªKݵ˜á>ãäŸø‚Vj-hõ,Ÿ-6x´×"GÁ»y¤ÂK¥éÆ_ÕŸ«�˜ŠÚ”mþoÚ´iüÖ[o �Çb±X,‹ÅúÖÀ½±¡Ù³g�î½÷ÞÃ8j �–º 8�%¨�'…Š�QÁ"×8D@L‚pLI”V ²¤ùRAÈ¥0¯Zµª `«âMCP­ðè¤@RŠª¢ZTV÷Q—í Àì�Hp¦°½øÇOGöøžú©_Å– ôTHVÈMþ3*6€õ šr <Á£}ÅÚðÔQ�H¶·ÌûÔ‰%h¥nIð ²wDéæ·qê�ò.µØÂ€d€¬ò3ð¤²Ö9 }�Kª±1‰¹v韕¦í³JÍÄÆB€ïø.|ó \‹…W 3/RÖùÃgà*>æÀœRÒÍ›… }‚Ú:ó—Oü¬=ÙæÐÞnóª–L�{¯ý>,¼<õÔSm,L€4Õ0›ã3fŒ/[¶,�‹Åb±X,ë[ �÷Ɔ¾ýöÛу:hXZ.Õ‘² º¤ÎJVŠº ¨� ؔ d`pu.-{À‰r ¶@�gv7cÔ\íëÀUub0¨�î† Ú=àX»°yˆTY@U{W¥À㓾½»œ© ö€+˜— Lº5Ø¥B_)ÍÔKXj)�UàÉu KÕ~àpÔ6år/ Ö¨3Pg��‹‹qºW|�¡~ÜCõ–òLý~#¸·�PÀ Æ-$Xl ˜ZÄ Žmp<-jhׂ†ñH[¦ÈóIÌ}onÄ�Ћ­…Ï+Å»Æ$Ž^€ÕøÌXöŒ˜S¦)æÞùj |p¯~ùHÙ§DëÓ¸(Åß|ûM›o‹ÒÄ-Š�ðçž{®ý&vß}÷ñsÏ=7�‹Åb±X,ë[ �÷ÆV®\9ñã�?Ž�_j+(-%¬\¤ú²ô]€E9Qî� � L•ô¸” Í}R{FÁ’ô^à¸víÚ¦(R:¥\ƒ\Š1ð’ÀÕ»´kg=ÆÇ:gV:nUe®ô`ª*°¢FúLéå¯gÎ;ï¼æ/êTdcå@�*Kñ§ÔG{T�å”Õ"€IÕÝÔP±£´zDºnìÆ�«x•¾´m,~ÀµZÿ ö /lÏ¿ñÆmñÁu€n¯¯qRQ-8kéßÒ£ùYE°ô'žbÎÇÇ{¬-Zˆ+ð¶0! Èj۾㫯¾ºËâ¿1Rüõc1€Â+ž`ÛxůöoíZ ðƪ„[4�×ü.¤„‹«¹«ýÏô!-^J9(ö·{ýöÌ¿E Ïš5kìË/¿LèX,‹Åb±XßZª@÷Ɔîºë®Ñ¯¾újØ~R€P�è`€XÙo ’(v�NJت³a«z³W -µ”’(…™2 ”\›öÅR6í_¿Þ“~@¤4[ œ¨šž¡#ª#¸¶Ðj“?€ò¸ãŽk©ÜÔa~J–B ¼A¸ò•ü6F@¼éëM­M`è9àÊ\wH­}²RÀO?ýôÖ7«š±´`ã�°î3^1•MU¶�à30��Sœ¥{ÎØÀ½g�²vŒ¹öR‹1`Lõ­tacíIéöŒj°jߢ…W{î?cå¿ ‹”qmŠe›s¯M�-Å™’Oᦋ 0ÖØ—A`Nø¤×}çy€î÷A™v¦²˜º¦O¿ ý™_q“žþÃ?Œ_rÉ%Q€c±X,‹Åb}kQ€{cC·ß~ûègŸ}6Li!TQpBÝ`��: È��k”=à%�•²œêxƒIp^Àxl@ðQkA�òð©jÅ`¨R�¥ØÖÞ`*+�“FË 2å°*KW•b€èð â�9昞Tdí;s˜ÿTcÊ,ÕqBÆ048ÔŽ�¢Ü‚B‹µ_™ª­ûZ/ ¤Zê PÛÿ ªka@_ ¨» ^Ñ/1òY5¸hñUhŒJk¼µ¹*]?Ü+�XßLJ7h¸Ú툽Xð‹JÍmß9sæ´¶ÄÓؙ͑7ðk<bçš{ÍŃ>ØæI¥n>Y�ä`×3^J¹¹Û·ß~{ûžJm¡£ªpÿõßþÚÙô÷M �µ©}�/v`؃fpmbæÌ™ãÇ{l�8‹Åb±X,Ö·�î� í¿ÿþ£_|ñÅ0˜¡JR©r� è�­ª|ìo�ˆ�K�Z/õtQ6©¨ªø‚XmKG¦Ûk æœ Ì€èÒ'à¡"‚DûjAvUˆ¨ @�5íJPZ’�0Ñ~Ú‚R÷ò�ü·‡´[@¸À)`å Цrç K/åöø‚2 ©M %8u|íL{Ú�­Tg�'EèJIæ?…šÊ üà·ýˆ‹›1�Cj)Pœ—_~y‹ȯ´sý³vÄÎ>mÏ‚aûhÅŽ² øõ¯=0on¨¹b©s nùî�l±¡öôò×Ë\»¸R­ÅÀ>giíà›?~ü¡îÛçl\â6²ÃHûM�˜‚w �î7æDƒæÉâlã³gÏ�Çb±X,‹ÅúÖÀ½±¡<pt×]w&UÉè€.°T¤ÿ‚)Ì`Uú¯ ¾à«Q@G�•âKe¤WéËÞeÁ.”* |< x¸Sø© ^9žHi‹bY•Žù£Ø’ãw€¦v,ØÓ.Ÿ@.íÅÕ�6(š \V%b0î~@ Ž ‚�µ„òÝþ\�IåÈà°R=k¢~ƒ6 Lív¯±ØÓ+Řjk/-ÿA7 îÆê$qWq¹Î*Ö¯1Q¤¥{cÌRœ�0Û &í÷½ì²Ë ¯[·®�ÙØ@»… 1âÿÌ/H6¯3ødÌ~æU_R¼A18¥ ƒ|}iôúΞ_/Ï�j ÆÓ]ÜL\ª*8Õ[ÿÚÑæ·Nô¸ÅÁ¼Ýpà í÷H>òÈ#À±X,‹Åb±¾µ�polhppptÇw]Ô;À 6Á"uŽr6Áœ"XÔD{t©|‡zh7Àè>0YûjA'˜¤¢kûNW¯^Ýú�dµ¿üpX©ÆÔGF=”‚ëš4ê‚Z{•½S ©œ�[»Š7L`Ú@X•|á?8«bY•~ Ð(Ü�Žê^íó½ª(tÀZ�ࢦ‚Tª¶v¥ý?÷€bþkKª5…Ô¸+Û \úu­âŸE�×ÁµE•‘ÅXûæH,A?`öÙg›Bí3�Ö¿´iKE—bc>)ææŒa°Iý¥nëÛg>‰g¥¦‹¥ØH#ú�I…»*„S‹µíE½õÛÚ§lâx̓XË(ð;±€`î-†¸ÆüŽø©}Ï™S‹?ÿüóøÑG�ŽÅb±X,‹õ­€{cCË–-%@”²> ¤ 0Á*5Rš-À¥Ú�! £k€ÊT° &}ÿŸß|ÓR›ë¨ àj@—>¨¥^`Ê»}¹ÔNê …”" ލÊÔÄo¼±ТR8ù ¾ÀÕ™gžÙ~€Ôý�hÁ:r£gÀªküâ3ð6æ+¯¼²)•žUÄ«Ž|ª”j fœ?üpƒrþR\Á;Œ` ‚gÏ`wú“¬÷‡g©¨L›ödSbÅØ¢ƒïŒÄjƒ©H ¨A" “7nlmRŠ]7nÏ›ð)þ'ÌÙs€˜j/•œºÌÌxç§ù©{,ˆ•9µ¨ÐÅÉžhmQøµqöÙg7À§4›3¾˜+û{�:¥ŸÂž-T ‹0öƒrßOLLŒÏ›7/�‚m3Ä�� FIDAT‹Åb±X,ë[ �÷ƆN9å”Ñí·ß¾)ÀJJ%ˆ`üJpTS©¾®96ˆÂG14�ÍÑ:À üà?∴ Y[Teª"Xõ  ò<¨¬ã„¤\ûLñ•j¼fÍšÍç{Ö>ÚÚÏ Ž€45¼Uåaঌì_þàâ7U•ºûÁ4  àÐþZEž(±*3Û/ËÇ:î dòÛ‚Á’%KZ¾ã#Èç‹~(äb”}ø@!èæbhÁ`ÅŠ L�2ÕµŠm‰•xƒGýSe�Õ¢�0ö¼~ÅL<�²ûÄ@J²ù±0A Öï)§œÒbÆ7q3¯Îö¬LÆî%ĈRýJ÷¼øy†àØbƒ±ƒ[ißî5ÿbUU°Åˆ[L €Ü+ÝL×YÆÆežÅ‘‰ïçŸ>~Á€c±X,‹Åb}kàÞØÀñÇ?qØa‡�€*J$ˆ3�Ì�^©Å É>T ^—g|¦S[Ø(’@ Õ¾S÷&©ºüqÛÿ)•ØQ> ©T@÷‚M MIu„‘ô[0&EXÿ*ƒ\›�ä µ‘Ê: Mí;j¯‹¾^|ñÅ–>M­Ý�M1³|•‚ko+ðvn0@–T`�Lµ �èJݵü©JÊÔ`P¼A¼qÏ�;·]ã3¸}öØê{K¦=êªÊÏú°ïšÿÆg´±ñÅ|)È¥MÐK�åPÓûÝkn�¸1±æXÔgª·¿-˜±´·ÈÛ[lNüFJ½Ö·wq§L‹“ŠÏ| ¯XSöëÌ`>b,}›ÕÞ_¿Ú²¹Õç�wÞ9öÉ'ŸäàX,‹Åb±XßZÎî� œuÖYpÀˆ€FðDÝtÒ  ²(“ ”�­×^{­ÁRʰýÀ ³Î­u­Ž5ö S÷¤ SÁ±Y@ë,]à¦oà –´yñÅ7UÔM`ÐÙµIpÄGÊ¡g@©ëTâÚ+ ò¼+`ÅgcÄà‹OU¤ øJ±¦þÚw 6�!?ìõÕ7.ŸÝ/V”dãbú£b ôùç3•´RU,u8ì‹.º¨ÅW ¨è H–J�•znŒ¥‹‰oR+~|�«V­j` ”Á¸q›~XÜ B‹ا4S]«�–¾(Í€™Š.VbÈýRr]iÒuÞ¯qŠ ®ãš�÷-·ÜÒžå·Œ�À©Ôúð<ð¶Ú‚„XxÖ¢ˆXR®y䑱•+W€c±X,‹Åb}kàÞØÀ<0±ãŽ;ŽPy©rÔA 2í$ °TY…� –¨}À ¨�ÑÎdá#P[û„-¨u ”¹‡â+-º*.ƒP{J�,uÙ5Ÿ©˜ HKßõü¥—^ÚÞ�6èô¹ ¤K¡jRœùöÄO´Ôfj³½µÚŠRpË(Á€TÁ&@È�.ü�Kñ¶ÞA 4c�®güËŒÎßÿÐÆ¶Û¿îÖùóÌ?7ßõ# Z�¨ð©Â±ŠÕb PYí5.ó#7êôSO=Õ ké—�ެ2ö*H¿à‚ Ú=ü3�V÷J].•ÚÄ Ð—òn.ÅšŸ”àýþò—Î.;ïÜÚ­”ò:Òˆr p�Ï÷RÐ̓ñ/¥Ü3Ú¦î_q×g¥«[`à—߀…ý/Õßb�å™rÿé§ŸŽ-_¾<�‹Åb±X,ë[ �÷ÆV¬X1±~ýú)ºR_)¹à¨3™‚[ûl.8%¥äCp Â�1ˆË�˜Â v¨–ÀØhC[¶öõzF{®I1–^KÝOTCÏÝqÇ ò€±=§ =�G ë^~ðpë˽@Þ}Ô\�å>j"˜Â àj«1Úï @�rŸì‰¥xƒopg|ú�~K©Þ {¾ V›[û‘›Ôd L ¯X×3Þ}ÅlT*2UUßæ«æÉËuã±p ðÅ°Úæ_·_æJ¼µßK[æ¯ö>æùóç·x1*9娋ï[12€š¯ÔzE®¨ÇàZº¸yðYz´JÙ`,ëÓñV>[¸�Ô~kÆÇ‡wß}wì•W^ �Çb±X,‹ÅúֶƱßo-zùòå#RŒË@}[3 :A ˜Ù°aCƒ@Ü€•´dŸb©z¥üm�Y…©<˜�¦¾©„ž+Ô‡"J*ß|óÍ›=Ó&€>×K@›ºÊ¿E‹µ6®¸âŠöØÒ3Ë*U�ª�Hwmªñ˽úë†×©Jîtæ^ðÙçJ{î6 ¼¾� ewÁ‚m_­ñÖB…ÏÚq•ú^ª»øŸ6ÊÏn›î’¶Ì‹¶ÄÜb‚ùòҞŠ{Í5×´ý¼C̵y¤äµYZ³Ïön;;Z!/¿йªÔ@W‘1ñ’:_Ž,lØÿ­Ù;ì°ÃØé§Ÿ�ŽÅb±X,‹õ­M÷ÿöضÛÀøøøÄn»í6´v«˜[3×AJ¥ñ‚�ëyjŸ"PaÚõ�¿5ÑR�EU•Â[©Ì”cª£ç;“@Y  l©œ”ÇJ«îOŸÁ›w*iÁ³}·R¬)“à×u~HÅÕ ´Ú+ÅTL_ˇRQ½¤—\Ïü–uñg»Çæ½{L¥»æÈ+–�)Ên¥šWšµ±¹·Ûw/@j¬Sa{ª2<u,®›/ñׯÊ­4k µ8‚eéã�ù‘GÙ|ü•Å÷[¸�âàVÆ£EØ>b ¿Ï€ÛïH_~_ÇwÜØèèh�8‹Åb±X,Ö·�î�ý©ÎÚí¶m . ‘¶\gÓ‚O)²ÀðRˆ .KYp`�Ri?(«ŠÒžÃÝÀÙé‚` ]E—´YŠm7€z�ð¹.-Wª5誂_U ºÓ€Ðþv HVzrÝWG71ý´N…È­Y�t§ë³v P«-ŸA­˜‰5¿;“g—Š\E³, P…ýÝ­X`OUœ·6çÓ©Ø Ô«®m•›UŠö·ÔòG}´�±ï)Æ ^�\ë×YÁÀö…^h l<ÒºísÖÙ€¯ŒÕ §SÆc±X,‹Åb±Xl[mà—_~™é²m±©êkÁ`7´L½©PÛ½?õ·ì·Ôi×Ka.Å“�/ß—úY}vû<Õ¯RçóKÚëL.lɶŠ·d5°]�O!/Ôvãˆ÷nAÁum”Âïû©Šõïõqjüë]{À—9òÈÞiJ.å�¬ˆ˜Åßñ‹› PMMwoU ·Xf�ÕxÆ cÏ>ûlàX,‹Åb±XßZàÞØÀ¯¿þŠZfu:�ÿêE‹S�x:xœzÿT@îé�»�­ ¼×}lÍþ(�O÷üTµ»ûÞÎ$�ÛSíš=Øî§´üniqâ�Z)Ò•º-5ž}ôQƒ_€|õÕW·^ì–pðÁ·½ÀLa2‹ `^®Iµxä믿þàÚk¯=æ;‹Åb±X,‹ý“Z�¸Öétþc¨Å\ð©}­����IEND®B`‚�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������pallets-eco-flask-security-72bf929/docs/_static/logo-owl-full.png�����������������������������������0000664�0000000�0000000�00000071463�15257217435�0025044�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������‰PNG  ��� IHDR��W���–���ó­¼�� �IDATxœìÝ œ\uyÿñ3£„¶XѶzÕV±ÔZ Ú€€‚$r¿)‰((R1ÄbñR0©RM@@­x¬¶¢m-¡[ë�(Ð*X³ÿ×ûçyö?ÌÎìÎÙì-³Ï÷õfæÌ9¿ËÙeç“ïóüž_khhè?ò“ŸÌûÉO~RýÒ/ýRõèG?ººóÎ;«m·Ý¶ú½ßû½jÓ¦Må1gΜjhh¨ši¯Õj•ÇlW܃{ï½·ÜãÇ<æ1åþ¸×[o½uÜóû[­Öfû½J¥R©Ôàj�˜`ÎÐÐÐŽsçέ¾ûÝï–/õx ºÿþû«ÿýßÿ-põ_ÿõ_ÕãÿøvSܘ± ,Áêÿß«‡z¨zðÁ«o¼±Úu×] \Ýwß}å‚­yóæy=oD©T*•J¥f¤¶âŠlµÕVÕ“žô¤êg?ûY�©þð‡®þþïÿ¾úÏÿüÏveTØhçú,ÁªU@–ÜçOúÓÕã÷¸ê×~íתï|ç;Õ#ñˆáG8W#I¥R©T*5#µ•/zÎÉ]wÝUÝrË-%,¸hÑ¢jÆ Õ­·Þ:.p žFûlÐ÷æÇ?þq™)ýþ÷¿_Ý}÷Ýå~ÛÃr·Ùf›ü¿&•J¥R©-L[qF<8(ÿú¯ÿZ-[¶¬úÜç>WBT¿ò+¿Rýßÿý_ß3’³ÅmIõÖOúÓrO¹…î÷Yg�U Šs(ër�×ÏþóYïò¥R©T*µ¥IX°�Ñ.»ìR�ôg'UÏyÎsª³Ï>»¸)BT�zÔ£úžR€ÚL×t�K8„rÚ~ù—¹ú‡ø‡VÿñÿQîùÆ�«o}ë[%$;gë9¾ª>ò×R©T*•JÍ,Í‰Ñøß~þöÕ¾ûî[m¿ýöÕ§>õ©ê+_ùJõ¼ç=oà~dáM%dé“#õßÿýßÕ_üÅ_§À‚Ú/|á ÕoýÖoU;ï¼sõ«¿ú«Õœmæ”�`ºV©T*•Jmyšã <’Ì9X ,(9?`@ÖùçŸ_œ•^+{ïGý\Û1ÎÑÎo¿®×y-£µßí\0wûí·h¤_ÿõ_¯öØc�²BP®Ûüùó‹«J×*•J¥R©-Osäõøâ�/r«Ö|Á QýÆoüFÉûX à7ó7G€‰Ü zä#Ù´£í¨çÔMÑ×÷¾÷½rN€GÀªº@JçêGŽ’{Э–W{ísïÇýj/·pýõ×W_ÿú׋+èºüàÕ³žõ¬ê™Ï|fõ£ý¨zâŸ8œ¯–P•J¥R©Ô–«­º%¬ƒ ¹Vê-qS>ó™ÏT_üâKž�Ò ¯ÃÿüÏÿ”ç¦0¥äEAÑnç÷ìK_úRIô®jPñˆ•wÝ®­êÄñζBÝ`«0Û¯¾“'ÕÞ†‚ŸŽU5˜µÏ¡}\Ú“gõž÷¼§:í´Ó ¸ÞsÏ=Õ7¿ùÍ®ò'R€JH0Á*•J¥R©ÁÐKþ;¿Ð9J @ØŠƒ¥˜(ˆøêW¿ZŽ aýþïÿ~9¦Èe¿u°@þýßÿ½úî÷¾[íüô�KnQ\Û A—\rIéÿä“O.ý�9KêA9×µÆÙ9‡qZ�|€ ` u޵Ý�j @û›¿ù›jáÂ…eÎ@O’ÿ3žñŒÊ ·+’Ï#iÝx¾ýíoWÿø�ÿXÀT™…—¼ä%à í/|á «ßþíß®þðÿða}¦R©T*•Úò5§ó‹=Ât�æßøFõ´§=­¬h\¬�}ìctäasÌ1#À©³­ÎÏ•y�IÏ}îs‡ë8u^ûÙÏ~¶Z»vm—w¼ãd€hÿý÷/E7_ô¢XŽäuö# §”„ñ[ýèü§?ýé]û qÄÂ!Qœº?þã?®¶Ûn»’Ü¿fÍšê�o|ãpxÒüÂñºÞœÚY¿~};`NÍÅJ@a@N•ð`‚U*•J¥Rƒ§­zÍ$€ËSŸúÔáÐ×àp”Þô¦7U_|qõÉO~²€˜kÚÕº;þøã«›o¾¹Z¼xq� {*¨ ¶€Ì5×\S½â¯¨^ÿúׇìø@µÃ;TúЇÊ1ŽÚµ×^[œ¡;lÙ`ÕÞÏå—_^}þóŸ/cº<òÈ#«¿ýÛ¿­®¾úêêìÀñy{è‘Kõ„'<¡Ì P ëqÐvÚi§á«£?m=Ûy¾á†ŠÓµãŽ;VO~ò“«¿û»¿+ç µ þó?ÿsÉ_K¥R©T*5xjmÚ´ m<lï:ÐÀ©¹à‚ JhLâ5·*ò…¸AÀ4€#.W<{È¥âøpÀÀ x’«%ÌwÊ)§T�}ìc«/ùËÕ×¾öµ! (IŽwÍyç�W@G‰gPÃù g´{ì±Åaâl¹^{†ÿöoÿVà�ãÒ8lVA~üã/çIB‡ÆkÆxøá‡—1H<·q5wIÆzôÑG—Ð3'ã&Öõk \9®›{%Aݼbî\3×r²–,YRí¹çžM\+7?fÄÑT*•J¥D½¢J[¢z•0—ÐÚ…^Xœ�bÕ è$`P ‚ `¸8\,müÑýQ�ΖÏÕÐàEˆ psäoÉ�R©üÙÏ~vq€n»í¶RJò8P"ò˜€žq9$éÄh ái�óª8[«V­*õº8Iï{ßûŠsä¹Tæ–Ì ü UjK(¿ëŽ8âˆ�†”à¯yUÎáDé_hÐ{îšy º`5VV®^½ºÔ³J¸J¥R©Têx¸ªêIZ¥÷Ö·¾µÚk¯½ŠC2„È„� ÇL`IÐ\`q™@QžßýÝß-îGˆƒcå!Pqn”fˆ¤p0Z8Hmxv¸žtžqqµ�˜Ñ6pãfÉ»C&€{ù�²ÝvÛ­\cNÆh.ÆæÙ8ÁÜu×]W }ê 6ÐeÌî‰uc4&ãp߸V 0J¦—ÀïþpÔvß}÷RD´j–o•p•J¥R©�Ö ÁUל«˜ ¨‘sÅqA`c¿ýö+Ä��¬¨..¹ÛyΠĸ^[B{Šœ!ÐÃ%’S¥�ú§*¹X'œpB í�“W½êUÕ�Xò«b\¦Ã;¬zó›ß\Úrú‰ü&PÅeÓ&'ÉæÓàp0ÎKÙºuëJ˜Ðê=®· œiC8Èzè¡%ùè 5,Î™×æ¥œ‚y£û:=ƒEóq>' Cšo*•J¥R©ÁÕœ^3ãè�y¾å–[ªßù�ß)ï�¨€Ô!:¶„ñ€“sÆ8G/ùË ˆx€0y[œ%ðÅÍѾð¡6l Ø@“Ї‰³Ä¥’»ÄÙJà °=9R 3áAýrÓ\ë\Ð5¦ŒÓ5 ŒÉ'sm¬dVà)ª¦/Z´¨€‘9;×xµé>pÀå>8¬q¿8_ÛÍÃgÂ�Bž±J0•J¥R©Ô`jëÓN;me«ÕšÛîZ*�d5 ‘Èm¥]@‚s�Àá$ £qˆ€—äsç€$.ˆ’ˤ�v…×@ gH‘£y\`Eè,Ê��äY±S®YÎ�²!@t�VÔ�ò¬?ã>òÄÀ'Ëy™ K{0¯wÝu×’K¥-ðиjÚ2~•Õ�“qƒJm º?Ü09V‘$/»ì²gæÍÙâêEH°³ò{=ÔjµÞÑý£T*•J¥¶|ICm½bÅŠ•›6mšE6}Ñ™¨ .œ%Œ&¸H *à…»$„ÅÀÎÞ{ï]^sœ¬�sÈà`� n�<.ò²—½¬8DÂã6�8@”‰�M˜�[Å…òÌñŠ„x9]@N¿í…B�R¬êÓ¦1h¬"`rÀØ‘@M2½s8ZÀѽ��ú&ÎÕüÁã|9®óŠ`KÏþóËX9VÞ›�z[qÿªþcÌ W©T*•h \µ~ö³ŸÝ×jµæÅ> áäÈâú€”(–iÕwIÈL ±�Ç¢°(°p XB~ GÈÏy¯{Ýë ” ± ™qžäuEí*pÃâö%r¾¸Y@G{ �¾¨½%Ô�%¶‘Ñ?g Ð8'*¬ƒ+cÿð‡?\æÕÓò92~m�50÷‰O|¢œç!AÝgÆ+?ËX¸ZænüÆÇÕ2•Ý«z+!�È1[¹re÷8 ‡fB{*•J¥Zƒ”Ð>'ÜžÎjêÀ*^ƒ°Áiâ^ é�/�p8Gž…Á’×Ü/`J¸Wœ&.�ü-IÞ>éK_Z ,�+ê¸[á˜q”äS9×Ê@ÐÐ�'�ÓÒ�¡ñpÁ|.´gÌúóp¾v�ë<î’×1nmuÔQ ߟþ韖ù*DÊAÓ†¡°žð¦ñ‚'ósÆaŒÜ©_š;·ÔÓr½¹q¶@™â¨æ”J¥R©Tj°Ur®ªªιâÀDΓ8L5‰p!r‹„¸�@ Ç(*µspÀŽ\-×su„Ò¸8Îåzq~ô¼¼kΕ?%lçzçr—8NÆŠ€˜|&n–k9[ È9žµ!| ¢"w‹“% ¨=0„®ºêª’OÃŕڪ47�fÌÆû(”�,㠷εö4�¥K—–z[`M¿îU¿{0vQ†S©T*5Шœ«N¸jWû–2U WÀ,± ›d€ÎŒRÀ H�ùŒƒc•Ÿ<+ #©Ûk¹K †sä<Éãr 8c KXÐ8fU 0Ÿkb_ÔÓr.�"€vŒ ¬Ø30Â{>hÆì¸ùpšŒüÈ“ŠêíV -º&€QŸœ0aLc°BPÅus7>×™#àRµýÕ¯~õp2ÿ8•p•J¥R©�Ö ÁUÏR ¡p°@вÂpHÖ-‘À.4&ä'Á�cÃÁ7€�Ä ;á4בH5ö|Á ^PܨC9¤€�Ü,m‚á5‰ð6mPQ¤Óxôú||„è´£ìƒö�g“ÆÃÝrTÃêÆo¬N<ñÄrÝûßÿþ„@Î^‡Â�\1ç:ÎíâjGcâzé_?ÀðŠ+®(‰ïÂ�Þ +ªJï~U¹Is*•J¥R³B=�+ žÚÝ+@Ľò�:ª” ‹ ‡ù—)Pðã:€ä:E;9@Bƒ>.œ.®–c€hýúõåZ $Tçza@çkG…wнûÈuÀ Ùà�!€#ÔhCfðÞ8Rr§´£ ‰ó\2ó�c`™h2 ø”§<¥ä‰=aÇ FøÒ|w?�›±€7.÷Œû%È-k_!8¥s•J¥R©�Ö@­ìµýMÕæ´t‚AÔf’Ç„@‹= d�N�áZq�öÙgŸârqt�•ó´ë5(áÙ ™³Å%cœ-a6 î±ÒO¸MøMâ:r½íx€Œ==Û®@4¥8Q§Ÿ~zÙvÆXµŒ¸O’ð�\]{íµ¥�¿øÅ%Œ ¬€¡±�1aI;óÌ3KX$êÓC¸’Ó½>í´Ó ¤£{±ÎU®L¥R©Ô@kVì-Ø×Å­V�œ“O>¹¸D Ç1® 7,�““N:ix_@@f›€j@0q=Hâ>)YÀñy÷»ß]Ú—¤€ XÒp²™2�Q.Á ?íp¿ô-ÇI8P˜R‚¼`JhR_ªªs¨.¿üòâlÝ~ûíeEbEå61`¨À–�©–>µuå•WàSšA­+óu\�{¶Ç{”× r3�«„«T*•J ´®ê› ü¥à¦\%À„$wƒ î“�›�sdsd®•r ¶8:!°â=°îó9`RwJ¢x”ƒ á8ç€3à¤^•’€Kò8@B–À ,Åö7Bx®ñZÝ­()Î'ÊuÜ&šë€@2vР„!…49`ÚŒm�ôeŸBP¨=°©Äƒ6«Í˹J¸J¥R©Ô@+᪾ Ä•3àN { ïyV0ó³ŸýlÙ®†CV|Æåá�д|EE¹>Q$¬Øœ™«´bÅŠLB}œ£È{²*Q;Ðq]5áJ |@“P%€ò¯$¬ =îö‚ݪGoûèá­q��± F¨(r§<ï÷¿ÿý2Fã�„BŸòδo,˜]tQùüŒ3Î(ãÑf5~ÀJ¸J¥R©Ô@k ŠˆŽ8Ò@BoÜÏœ%«ê¸<Âx�*ÂiÂbÂl JÙ�#aÜÍY\.ÕØA™ü'¥V¯^]VàÜ$¡ƒ1îpãŠ9D9t ( ¤-¡HÒ¯•�€Íu¶õyâžX@ÎHÐxÍ@YÁ(ÌhnÚbô0f+ …5¿ó�ï”sô%¡lÙSÐy�-¶r®ù¶ï+X Ø/T*•J¥R³]ãr®À€Dón¸¡8@\ "D&÷�ø�œ}öÙŹ@Œƒ¤Dðd®áð¹ç‚!NVBtjEtÐAª8L\$P$DÊŒ   r¯��pÀùÔ§>UÂz×_}y}ä‘G–¶–v¬xä|E‘QÏú6”T¯ ãŠjnÅ^‡Ú4 è˜U�3Ç@šc®?ðÀK~8a 7m®Ò¹J¥R©Ô k�Œ†ÆYÖ&šä:I`ƒ{´„Øä#I2÷ÚgÑmoƼ,Âxò—€”vµ^„�fO v~ðƒ,®˜ö€“0£@"t®U€’Ó­*Ô†¶¢ÖG>ò‘êè£�.ðu¹| �+,©+ÍQ{` |�A0äZPhüœ00÷ö·¿½€pãPq¾� „z.š�¨¹¹Î{™J¥R©Tj°Ô³ÎU/�bÿ<€Bì9¨�&x�JÀB2·U‚Љr›� qœ›°!JR¹ë$«€&”& TlÍ9â” rÀ8O€H_` ˜Iš?î¸ã hqŠ"‘üâ‹/.áF¡:aC}ƒ*×xÏ…lrÄŒÑçB‡@Íç'cu¾‡ð&§Í˜Þ­·ÞZ®w_„#/˸„>e?}ð§e<‘Õ@Yç*•J¥R­Aªs5®ú�@üp¢€‚\"Ž Ð�xþ®w½«@‰äsP[àH *À%7ÉP9�-�Ãã q¡8F Ë1á;°¥`Èä:É¿:´ç¡>®»îºrýM7ÝTB”B�jo�$íqÌÀWÌEØ‚$®xŽ‘¼.ù=ˆ5…E�¦òú“ã彤{ðÅ3g¹f\<zÄÜGd•öT*•J¥\�r®Àƒ0ØÛÞö¶a‡(É=âÒ¨>¸Bï|ç;K(Nò¹¢ž�¨ªÆ}ΕFÜ'HAŽ2 @H»^s¨ä> ¤ý�~ô£®�L=)ŸÀ3€Ï¸©Eeep¤ü‚¾¹^r¿´ã|c±�È’g®€!8Šv½^¼xq9Wø�Àš0%Ï}�a‹-*ŸË·ò™y˜§}ÍÁý‰Šíc�V[ò{æ\¥R©Tj 5H©2�Â�‚Å©á��¶Ü˜sÏ=·„ß�„me8JBb±/x>Ó†<ªµq‰€èQÔ3`Ó&H àâd +rž\ÃiFöã,�!…B…÷.¹ä’~ò¯Î:ë¬R;Ëx¹J Íµæ ¬Ç}Òž±pÒ8S\2ðg¬@‰¥&à1 ¥Oóq�9úÌ5òÀÔþ’Xï>h‹«'‡«Ÿ_ í¸×õöCL¥R©Ô@k�‚�á 8p�@Ž‚™ò¥¸0œ¡4nPQ¦@qO+÷¬Ò“ünµ�„öX� &®'NWH‚8°òxLV:_î IXçr9Gò»÷àÌ*>åô)ßxpÙo¿ýÊyÚHò´�¡ü1×ê×10f¬æõ·@g 8q¥<„ÕÕržûÒ8]U.Bhpõ£Ø0[Û™s•J¥R©A× ÁUßu®Âmá6�rÊ)ÕË^ö²CB`Q{ ( {qŠb»˜÷¼ç=xäRqŽäeÉÃâ�¡;0â\×ÈqâÐ# Ä€0%É\EøhS2ºœó9VžUjF@œ�?¡Aí rÁ€žU®T‘¨¯Pä@¤M µ«äqEs:’ƸŒ�3Ø|.œè½p •“ææþ¸f,´5(×�J¥R©Tj¨QB;ÇH‚8Xˆ-h83Âi’¼å?�–�Ÿ¾s ÇY½†”)P�kr�(@�� åZm*À�RáüœsÎ)Θ•ˆúQÇ °È•h@�ËTµm.í1Æ –$Ê [r¦¼JàLY¨ã‡€FÉ€¦>•\,ãXW\qÅpM-@eLÀ�ãn»íVÚáìi›C§/+c³iÎW¿aÁÈÉ€©T*•J¥¶ õ”ì-_HN¸ $(*|fÅ\lc#ì�„â�–ü(!9nÍÞ{ï]ò�¼æZiKb:7Iû x9‚/×+ êü—¼ä%œ"iÞçÀMn”tàœ7N0ÄAã<¹^xÐêF9aΣH6s Ž8å@Bãú�ßü�jÇÇïXà, %îð¸ <Cmƒ8Ð)|)ô�9YÀO"?ÓÇòåË‹[Ö-‘½¸"™Ý¸6mÚôÐ6Ûl“aÁT*•J ¬fe)"æK0qc@�¤s�”8,�  p¡$šƒŽ‘Dpn“М}ø8I±uŽsÏÜ/@Â5"|Õ«^U€ ´p˜œï\Nè�yÜ00%d )ZQh+0c#icRГôï¾ûlj›‚ŒÕ¸„ò€�²Æ+Äø¨GþbÛ wðÁ—Äù�÷n,ã0÷h’P¢ëÉ<ômìNœ¬^ÒF;tÅk÷ÄýK¥R©T*µe¨/¸rŸÞ÷¾÷—Ê>ð�'t$™)aB�ãÐ €CÝ*!4 %|'W)ÚµŠ.6}æÉKâÉUÒ–Ï\Ã)Ò>Ès/}éK T�n‡èðÃ/Ž˜°#IÝ*m¸Fc69UŽ�/áHóñЇÕ�\+ÉéÆl’Ó�’2 B…î…v—Ïb³g9UÚ4feÌC›½r¨z‹¼«T*•J¥R[†úNh0Ât€EØ <�·ì Jæ&Ÿ à ��`å5Pâ@ ¯qr|›s“„êÀ—Ð�šT€FÞ“>„åiI*÷ÚjDnšÐ"ÈQoJѸÀwL4 7cPä4Y1(ß X}æ3Ÿ)0¾H’»1š+`Ó¯\/ΗI¹à#t¶¸rŽ �ræ´ AGOXÑû^pe¼Ý ªÛ±T*•J¥R3Wc:W@@ø ŒÈ€X� Ô^@TxP×&œ¡R¥}ÓÏ xh¬ó`Å¥]À d�*% öÙgŸ4-�ÃÝ_ÇsLi““¶zõêfÜ*c26!Eî™°^ܵÈÅ]B…Î5�ã<aC@"9t`H¿q޹‘väz�8î\„�oÀ8ºfÏ=÷,}ºO‘ ßM Q©T*•J †ú ‚!5…7…ÃÀI zrš�J80€�9—³´é盆7iæ ¨8VÂfò«´æŽ=öØ’0Œ€‰íl€šúWàM¸Qßo|ã‹ã$D‚@(Ò׈¥€¨±s”ÔЦ«ëG¨ãhÃa‡Vœ+ùWÚ~ °dàŠ;eÞ@ÉæÏŽsÖ´aoC wÛm·•¹ÚŽÇ5ò[• •J¥R©Ô@kL¸@ÃC¢8wJ˜MP9n’@C¸Ï =ÎŽóÃÉâjqu@¨2´šPˆ q¹€N-áFPæ¡‚ºvôÅI’p®}°h8N®ãVE)€÷⿸¸_§Ÿ~z/WK;ò¢��ÚU¶È^ÂŒœ'î•U‡ ˆ e®Þ_ì5ܸSÀÐÜŒAΕƒ…FµãÁÉëå\¥R©T*• õUŠÈ=²Š Å{�,ªÚÝòžƒ#ɳ•uB†ò°€@â*¨-%ð$´æ3€â<GcãgmÙ«LiÇ5 Hi¡Cচ»ë«ºÆ€3V€Å‹äzFsÁ8O@H[rº¬4ÖXÉÑâ”™“ëÌh NPÇ•ã¾ùL™€ ¸{Ü/aS¹^ÀŠËÛùŒCY¡=•J¥R­YU¡ HF¿ôÒKK^pRL“;%T¦J:à�DÀ„ën¸X’Ü«:Y[;�|DHϹ�ˆ  ?:¦Ì‚v9aªÁËÁ’à`.¸à‚2îÈ‘Ä^òº~þóLà ü�çƒ3ŽUùú<'žxb�cµêP�Ê3pÀ8UÆà´ "#t  ¸nì¹aÚB<ï¼óŠ'™^ŸrÕÌIˆÓüR©T*•J ®Z›6mº¯ªªy£Í´€n’ºR@(¶rQ‹ 4Ø?�pd»ð%D\$”ƒî@4 ¨úÌŠ=TÚ9ú�¯$·IÛ�Ȫ@ã�8� @�n“Ð¥~õ¹Oœ&ùSœ0aBP$GŒûŽVä•yωaB�Ü*˜Ð û�šÔø KN•ñHV7ç4Iú`Óx%µyä‘e¼½VŽ¡û[­ÖcF?%•J¥R©-Wƒ”6ÓW)° $§œ�Ç øÈ}n»êª«JhŒ[”�ˆP¨p®œ)°$Üç`Äê90b€X6-Žs–¸Púødª½@g\ì@‹s8u-— d=ׂ=ð&× ì�6 %÷Šsš¸aú”O A�±p°ŒSx3VOFÝ-EI…ÿ´áspåáG¯žùyÏåK¥f’,pi€~{�� �IDATEÛëU#>}¸î¬ªj]}Äó†g¤R©Ô85jB»?¼@I8, ± Ð ·è ƒ* Ž„Þ¼Žíi<$¦ Ï3«9@@ (� �Ûá€À" 09.ÿIuõØÐx¬üsìŒ3Î(Žh9Æsþùçx:þøã Ð*Ðd,ò°ô¯]cà,�?®°Ópã®q¡ôé5¸iæ­�í:›;Kj'`‚)ã‰úW¯}ík˼&{�ÀV«uw«Õš†Ç-#ó‹ñ,n0–»G4�ê÷羦Ëýìõèú³Ú‚µ¬ª*sª�jU`E ÛÎõ»wo góGœ™Jmž׿Ÿý<&úï ÿ?ÖŒ8ššt�ê\ÅæÁ? CR;P�FÞƒ'á5 0ªzë0â`åàe¸[Üï…ô\’€�U{r˜$™ƒ) â87É9Î÷9Ø“ÓÅ©2ªŸ6î‘<,càV e Ê­R?‹»eüÚ[Æðßþvõãý¨À7 4É¡ —Š«%Thu¡ŽàÍç`´ —ó9xŽé_Ÿ  Ð) Ѿs*•Ú,-®¿4LÐmœß[§TUµzĩԖ£%õ?ü#b}þܦ^£:WUí^Ž×`GXí½ï}ïp](�´ÿþûxòÄx („]²5á Û�Ž(=^ ¾èE/*ÐÄ…ºøâ‹K¾—ë\t\ç™;å:«Éy€�cÆSÀ”Û~$«ƒ#í 9>ùIO*¡Mãs®ëÌ�{%ܰ„�XŒªðÆœ´ ®bŸCð% �:αâèÅ„YŽ!•Ú,­ªÝª‰«NEûéb¥¶4�ª;ªªº¦«Ô4©¯"¢Âa�ƒƒ.$ossÀ“=ü$“s˜�„P˜�‘oÕœ~� �›H3œ&àÀ&”Š u½s@�>$·ÛÿÏq0¤ã°"Ïø„�ÁgÂu ÏqÏJ>¸Îû¨S€8OàKÈ Eb{8Pœ/â I¬U`ï²Ë.+× �ª¯…>�IØQ.VHÕ(ôY*•·ÖtäVM–×€•Jm J¨šašÓk¯»vqc<@†’ êä“O.ÍÒ¥K‡÷ì“°ÍÁRŒSy€ÅU’ôÎ)'r”œÇâx�cà�qÅä4I:?ôÐC ŒÈ½^>u\%À"h…!`Jt�ZS€†ÓÆeN@Žëäô °”|²SëJ`+% Âaӿ뀞cÚ·‘Œ)ö/sÜ7n—MÑÒOúÓå˜ÏÝc1W÷:Cƒ©Ô¸´¢Î!™*-¬anyþ¸R3T‹ëÿ/çhfiN¿!ªp€80 èmo{[õö·¿½¬‚\�Gy€¨@‰¶]ÌÀŽ0œÐÜâÅ‹ €Qä6X“×Ä%sLûÎáNqÈ„Û�Œ\+.•pž~U‰Wº�3¤}c�Æ•RSKœ/ípÄ€ðáL9îaE®”±r­8fB~Ž�.ý˜»ñ€=ý [úxù ðq±¸`6ðæž # e&X¥RãÒ‚)r¬:µ¬^M˜y+©™¤X�‘P5CÕWXÄ/ ü€î÷FÒ87+`%¶»áòp’@ 0r\QØí¦›nª>ô¡U_ùÊWJh�ûœÀ‰6åLI„G œpŸRQ# ÀEÁQ«Ø²4fãÑ(WŠŽ 7 ðr¸€ˆ³ ´ñêCŸ’æõ)Ì'üçY{œ2!A}j�3Ƴâг1ù  �8`é˜g}U™s•J�GÓ¹’o: .•M ¬f¶F]-ŠUsB[`FþGLÄJ8î Àâæ�#Ž8¢@ €fÎ’Ä�çiÉ’%ÅÑZãø�)@FÔœÒ6_œ$¡5`ÅYR‚AøÑyÚ;ꨣª×¼æ5Õ¹çž[B�d… ¨…Æ ºä])¡]ॽûIˆOB…@ŽÓ¥m(?±~ýú2—SO=µÀ—ù“0!¨’î`¾X‘±¿ 6#¡¿óW¹©s*ÕMó뜒éÒâÚ9ËZX©Tª/m%/¨Ÿ/t@�"�Ð�Y \�…¨Ä.ß( ðñ’cL”Nð œä1 +F�NpØÀ wHØï¤“NNxK\©Ø«ÐT‡ò“$¯=!<si±3 âŒ�®ˆáªÉýâŒÅ–<ƼË.»G‹kEÀ ê×µÎQ³jÕªU¥}×Éõ`ÚŒ¼+óñ¹mp¼÷0.`Õ«ƒû›Éî©TW-‡k¥”ÂöþÝÒöxf]fa<šN¸KmÙZßñ{8Úc§üY†úr®À� ªêÜ+€Ã ²õ ¸áj[Ë€-á9.–P0j@>(@Œ³õ…/|¡8Eà…[¤�(‡ tÅÞ|®{Æ3žQܮؒD�®/|ñ‹ÕË_þò[W^yeÉ�ºöÚk‹£œ¸OÆÊ9Sê^É‘26 %/ʼԾdàfåÊ•%OJ™¤vãÐŽU‚Ƽï¾ûV·ß~{uÌ1ÇT'œpBÙóÐX€¡p'“4oU#ðRXTè0 ²zôʽg�ŽÖ$kíÐÐP&ë¦fºš–\XÚV}½]w¶UfoZÊ!W`¥R©¾5'¾ðû·H„ód£bIã\¥�ð±ÒO-(!1ÏòœÀH>VÏ]rÉ%ÕqÇW%9[ ‰«^„Ô.ºè¢²é±p÷d�ç  ¶ê1�ºØ`™;ä3ð¤]n™þ¸Q\±ýöÛ¯Œ0��ÈyT^-ã÷‹ûº¸o Éö;Ærúé§çNØÀ�?×pÝœ§-ã�ƒf!��t¬@YéÑ ºRc«ÕjE^Ì‚>W”µo²qhhhíˆ3Æ©V«5¿m Kúøbn àÝ8âŒT¿Úجڵ¡^ؤÔÂæÂÕâº�ùcäp­oKžŸìmy–Ôÿ¿Œ5¦ªÍñÛÐÇý�-ZPßÃö6Ö®in©4Ë5ò[¾‡�€FBœ*P¥¤�)å €ƒóäPÉKò^ò9G|p¢¸Oœ+ï9;ûØÇ†÷%Œâžrœl¸Ì)Òž6¸`ŽnGIß@ ¸I(—`t¸k€JM.탶ƒ>¸äH&ã<B’±Ï ó�ŸÏ„3¹oúK@ d™÷Ëkss?Œœ¹Q€Ô¸�]Ô‹U”ÚY¡�©(Ï�`ÕL­VkY«ÕºÃ6/mÕµû]ªß¾ýIlãyÜ…)c<õV*Ñv?_Êíc¹×5¶qÖÔhEƒ­:Ú3)lÖOq} µ�ÚXƒDçc<ð=¿í~ÞRÿ|Ç‚˜ÅÛòÜ=Á õ 궇êšHýŒ©jÓ5õµkÆœ·tù½éõëÿã=®ëõMýþηoM³¤~ß¹ÕÒü¶ŸÝšúýDlÓ9Æ&[Úôê?úZÓå³ÑMBô׌ÒNçc ô·êǵrNì›gÕ HP"A>“÷ p�¯Ø�I€ L¹†se… p W€D¨À€‘õ·®/0µÛ¢E%§ Ä`|�WJˆQB<WKûr¶€WHˆO»/{ÙË øX’@–ñ^z饥O5º@�j�Ì<…+Á"Pr·ÊêFŽ�~…¯»îº27 eŒBˆÚ&6ê”o¦b½² QÛí+#Ϫ߼·Ôðïã‚z�¼ñü�Mþ ßÝæ‚õ¥IO)`ÙjµúÙ#o"µ¬Ë—E?Ú}¹óë?èý@ò3»ä¾l_;ÝM´¬ ²7GC÷N�À®˜@X[ÖV´r¶U²_XÏ{,-ÛBîMÓÿwûý=l²eà•;é»x±ê X 2€0�2�Eh l� ç€çÇ�/.Ñ!‡R’Óå(ÉO’ÿô†7¼¡|&Ķããw,ùN�Ýn»RD Ê^ƒ`.6F\€Ì؄՘ү2 T ý�&pääfI¸—wð�à‘GYVšƒ±[�hL@Ñ8…!­h<ðÀK—‚¤¯{ÝëªÕ«W÷M±P×jƒS¦œ03«?øÁ–{Ž•±Æ=mfqÑþÔjµÖÔ'ó_9«ú›Úéºe’Ƴ‚›6âèähá86xÝXÊdÿQl^Y\CÄ-ÓP`q~ÝïDÿÜÇÓîü6çl¢•Á'k+¢™¦ù}‚UU»�[Bh°—‹ÛKýþÿÔäÿ» K˘)ê;,ȵ‘ÏÄ)’Ä'p�…8åW�$Ç9@ FîT�g„ ÀÆÁ:í´ÓŠ»ÐäF9Îr*Ü0ï¹V Ѐ·X½èµÏ� ãÉuâ(�¥W¾ò•%Ôç|ç‚.ï�!�Ò~äDq—�šÐâG>ò‘€rÄö¸X®–7Þxc¸GÚÞü�°½÷Þ»„*ÍM(ÔxHÿæÖKYÿjtÕ¹LSõ/åc…æêñLæw´¬©“6-ÇV/kǪÉåñj]Ý_S-nÛ'0B+&±fÖüIíв_îS5¦SðÿÁLѲóÜ’€¡ÉXû]½ÛÄŸ<¾¾à*¾ôA °®º“¿$‰$·+Ø)4:8IÀƒÛŽR™]29 ÈœuÖY¸Àš½ù„ýÀ è&BŠ LÂ< Фwá<×p‰¸a`M‚ú'?ùÉT`K1PŽ÷‰ å5 Ø�(ãiÊ=p»<ŒSÛ\10hŒÜ2Ç8W ëÍo~s�+!BáGv(Ç XrÖ8`€SÒÿ²eËJø�ce~Œû 2§QËê\£ñ>¦ú�éª)þ>Ö¿ô›ü¡Ý¬qL⽞?ŽMЧ¬BMÃr½´ª-Ì6Ô ç¨MÕ¾n�IÔ£é–)Ó‚†Ð·%ªŸÄÿPS7hºÕä0óû„õ~C‚ë1ù¿oç p¡ä7EØ ”� à!9)§à8w|,ï]') q“¸XÚӇ鿛o.ÐAgJÎ’s´/qžkÆ=ÒÞ{ìQ€ œÈsNòtŽË�Võ�An–vÀ!°Šs—±ƒ Ÿ+« 4„ñê¨qά`ôŠ­oÀ•)‡PiÇ}‘ÀM{æ/ÿÌqPîX»š¬ÖœÍªáb*÷•£…c¸Wýþ¡õ‡k§¡¡¡V<êdé&nÌd¹WM‡é�«ªþ—õd¬¢\Ñ‘à=Þ¼¦©?®èšú]P1QZ8àÕìç7øGÈ–æÄl‡{5š6ø»2�«OÇ„+_ü (H8+Â_�Èpo� ˜�WQ¡ÜÊ;¡:9N�ê°Ã+àp„âÞÿþ÷g4i/ÂvBƒŠƒ‚ùYÊ,€/.—±8Çk.'ÊkÇôõ¸8U2 [ò+šƒc®“ãÅeHÜ)×Ùr(qĸMΓ¼.wÌ1 $ôéõ5×\SÎY‚�îꫯ.C……9Ý}p²ˆ»5ZX05ª¦k5Z×?"uîW_«Ò†††– =ì_fCCC«ë|¥~�aY†œ0µZ­¦NËt�Uô½tÄщՒ°îhøû¶`’ò™ÆÒh}NèLç6E3E¶Ð¢&cëÿ�®7»¨ŸÒ)[¤úNhÂaÜ&e€“Ds p¢R;ÐP®€ó¦¸U±_ ¸�!®åް‰Ï?üðÒ&wIm)Ž�ü'Ž•c®Š‹rúˆ²ú�äƸ^‹ú–¶çž{–>ÈçÆÎ½ŒQîAhÒ\9W\-à …'Õð2.Ï€Q{@Q^xaMe\ ö@@š+0hìÝj\¥úRS¸ò‡bi»[T;FMá ×‰~¡¤g_5pµ‡»Öu)Ð>‡ snê„ý&÷tC ƒ=ç3Z_×§šlÅj°~óûšBLÌ£s¥â) Ã#‹GùýX­«ÿÿhÓî ¿tçOƒÃ<Ó´¥&g7Y±7Œ¿�£}Ö®ñæSÎxõõM/ô€€Bìù0$Ÿ{/4'd,¸D Ã*9ED…É$vƒ På �–Dq!3P¥ma<�yK%a4 ÃÝq>—<Ï¢“,”@  ^r®|fL\79`±��þå‡1NÀ²:Q_ �éÓçB�\/c 8òÌÁºí¶ÛÊ}àº]uÕU¥@*ÇÊ\¼gÚ9Zæi,UîØXµcÓÄaY®òüÐÐЈ ­oè€ln®Ó’Ñ'V<-­ß·?FÌas¥WÃ/ß õ—ìLÈ‹X;…cég%\S˜8e`Y]o²¹ð?ž½—×ÿ_t~¹6�ÚÙWëêŸW€èòúgØíg»¹ZݽM~½¶ßé¶ÕÎD¸WM~ÿ¶ m_Ûßpg$m ý Û åq„À À†SÏ Ìp›� ȉ ‹¹@Îá.©²îY^”•uBg xª]%÷ ˆp‹À“Ï8J@EB»¶¸L 0éØ@pÑ¿q�·Ýwß½„í$‘»�É£] zÚ¸ÙªFÐeB}žÁ›/0d¬Îå:™— r¨8XúѯyM€æz°è¡¦—ü20§ãu�ó…²úSíØ”Ä´:ª´ÚM×�Um�cÔjµ6Œñ¥ÙÞv7õû¯­uaÐÕa�w†§X .çŸI`Z_1¬#46Š•p½B¸M f}Ÿ‰ùKG)&Ù©e]öKl 7ýI][ƒ\Ìwu—û±¥%qoŽVw¹ïƒPN *Ë÷󷱨�v¼SWÛª]£:W`#Ü!5á1!:Éé B¨ÎJ:+ú€‚("’eÀ"6_¶ß^užð™0"‡‡ëeËà$GKN—cB~B€®N\­8 ôÍ!YÂo Eé…C}΀ˆv€Ï >.“P£ñ  Mr©ÀWÊ<#ñÝ=´ü¬HT¸ÙÓÐØ…=Í?6“ö�a\:ù^@R8•Ûç^pÎrƒæñ‹óÔÅÝ9¥ÍýÙ}´Æ•5¨ ~n®#Õô�B€ÀÝm+3ï­Ç³b Ê-„šÎ»i­ž&U¸»=š€Áêq†Óšj´œª&Žj¿¿3@Êü.?Ó¦ytý®Ä\ÚÂì,´:[ÀªÉ=ÛÕ/$.ìñ»ÖïïßÀÕ¶jרpUÕÛÞ�á-Ièœ ¡2Î ’¶SÊk`½‘þ�| ¸C‘%TÈõQÄ3�Ì€)àz¸KÜ1®èt ŠC¥¢ºÐžäuùV€‰Ë¤?yUJ:èü‹U{‘äî<ã��ï5 ‰úôqÉ�— 8½þõ¯/a¾Ë/¿¼zç;ßY®;ù䓇K>€CÐå| Èq u”’欮 6#·‹ó—®ÕÔˆÓ@Õ¶MÎf¯êªÝ´ÍýW×ü¶Ò�«Ú kÌ[S¨©.1E8m§úK2¾ä—õø½i2«ºÀd¯G“v;Ïmòóè/¸IÒd­\�)j2¿nÿOôû¤�Þ£²'\Åö,±­ÌW\Qà`Èmâh � �Ŷ0`Æê>u¢œ#œ'\¦Bº×ÂfÀD[Ú"$'Kø�ÅÙ:€„£¤í{Ïir G ÐDn–êèàËêC!<.`3F@¥mmúœóÆá:çÝç7ɸÌOÉŸ9P¤Þ•>À· j{íÚµЬ"4áEm; ]ÊOwò¸¸Y@ (ƒyrToCNн5LÝ2Q@ÕE“õ¯×Uõö7·Ô«§[½\›™¦X ÛÚ,­ak¢þˆw ÎðìC“1åæÂÍ5è]“|�ÿOô½ŠzÐ÷zÂUlÏÂÉ:‹ý�…Ø€Ž½õ€ŽÐך5k @�'ÕÍ]Ë�ž…ÕwýQGUÎ>\)€R)Âp`Šn@�þœÏ‹R @Kß‚úXÎsmìwÈùâ”E�Sv€ß3v~Fõ‚¼ $ªsÊ´ÉMs±r‘;õ!4ikp:…!Y¬x46ó�hÎñZ(¨}éK_*ãq½ÄwíFm«iv°Öv®¦kø˜Öÿ9Ú\©»k�º»Í šôåàurüd†�áuútjɺ©êºúçá¬gnf±\ÍIJMÆ”pÕ\›'Ô¦&¡Áöß·~ÿN ´kUõ‚«Î–€�8UÀâE{îYÀ,w ¡� Ï >”?tÀŒÕ‚Î*’Í%°s—@ H^œ!px€‘k�X èT\"ããtq–\oœ"õ­�a¬¬j€‘ÿ%A^¸ðzè¡¥v®å8�¯(8jKszÅ+^Qœ9æ­Œm|Àèò®ä”£{fÎJ4DÔ}îþÄ=Í¢ÍUU»+5mî�|¯)­¬™€5U{N¦îl !ާfV·ü¦™ Ù^Wj*µqÀC‚¡;@dû?:ú�«�­mÕ®«#ÈÍ HDÀ€RÎ{ï{ ¤È½ @#J„Û(@’°™šv�Œ<,{ý�á6¥¢°&ähiß{!; zôã½ëäI¸q¼@xzt èZ©®8X>AÚuž�D|ý�?…DÍWßË—//!Hý. 店¾ô¥%GÌx8Væíž)R*têzí<¹`Ú�å�™£Ð¢ü0î^æ_õ§:iÆí¯ìC«Õºs’39Yë§Ñ-\P'åOu"ïh‰þ�ÛÝY»Sýh] XM·lY0ÝžÙWÓ5çÙäô­ë–×ÿÀìw[œ�­mÕ®®Î!1®��@4H*æ à%Ê2D})Žgò­œ¯ô‚óuHâ Õi›ã¤Í(•PÕ°P„ÑäV�!a5áG¯�QÈ-’Ö•bà,q¤3>ðæ|.ðÓHjÔ¿p#¨ó( ;4sU€(Ê0h[;\5ï¹xñÁ¦¼+÷`�=c4^p D£«Üµ,(Ú¿Z­Ö’qì�×®�m…9·Ÿè?’Ê? mßVëf¢Õd_³ÉÒŠipnV´'ýw<º­’kòû±nògfâ—D“1m©`6]âlp­Bý&¶/n�VÕlp­ªnpà töÖ·¾µ:ûì³ °€á90eÅg‹$‹Kű’O„¸;  p�E8%¾Ëc ø€†ëº¸\V$J2fàGé!@pÄ©3®76î•P °†“Óå g¬Vò�&0P¥Ÿ( ¡=Nšó8OæbB„@JÈÏ9B�Ƥ4„{#ÏÌJ@EDÍÃüÜŸ1a?µ¾À#ØÒ¾U�@ÓëXiXÕ+1õ‘]uΦa©õ5H=³Î#Û¾­täý�¬!딺üöm@70×-çgsµ¾á†­S xMï[Óðéæþ.4_·ªìñè,(ÙdN3u%èXÐ7] =f[ŽZ?ÿPœ?ÆníèÚVíWU[}+ÎA(¸GVÞE9Ð`@PBmQ2A‘PÎM»SÈ\/Ü&<æ:ާp ×yÏíKq>G‰ƒä¡6m’1€»(ÐÛí¨â\y€&áD ór¼@•ÕƒúãP,på\}«½Ü8j\2!G¹b`Í1n‡ŠÃçúH¸7^Pæž9g¯½ö*à›;Ç‚�T_ZÖà_ØwÖ›$ï^ƒT/gb*Þ7¶ÝN›6�¸æOðêÁõu�Ð&N[¯’“¥ñÔëšVþ¯ºüÌšü § zýÎwS`_ÜV6âŽú^w>&JcÝ«éÚkt¶©_—iaŸ?“YSú£+\Eݧ(‘�¨l¶Ì�P€Im+IÞ�l‡v(a9Å4¹5àÂõò­€ jE âöD¹®`á‚�4á=!AáEý/gŠÛL\ãZ%<;‡«š„æÀ–±q’bn^+ã5ã—œ/\*¹Wœ¦óÏ?¿À÷î�o|c�29V I‚:[Ü,í�>UØåhLç€IïÁM¥w×@Ë„ö¾Ôäéò±ò’êäði ‡´W�\Ý &jÜëÛ ®6­Û3Zi†Î½éš>:ÿø6 Ûͯó¨ú¬¦ù{»ÀTS�™ §¨É˜6pûÚî »„iÛëxm.h�v¯¦#<½%ksþflè°úݰ{V„«n íf� ÀCBYÂcª“sˆ¬ö@œ#P£öÓÑG]�Éûn¸a8]Ò7È"ÉäÆA°âf�©H˜çPÉwr9Ü"¡H€f\%�‚„Á›±ê7¶°KBpáym<`P¿-Éçú•ëeNœ%ùSœ3cð ÊŒTš; t\it2ý O‚©€N�Ç•ãøÉÇ2îk®¹¦ô/!ž{—ÎUßjò‡tÁh_.uîÖ„­|ëQY}AÛ–ínz®L«·âñù½#>œ<­ï¨d§Gƒ¦vÅòTü+4–M¾ Öe9V××wÂëŠ.Éðý¨ÛÃÚ÷-Ào÷1`vUœlèqŸ×u½PS·oMáš5 �·n÷ª×x{鎎­y´ýìf»FûýéÔæ‚èÚ r ïœMaÕÎ8ñ¥`Âù‘|Í9âÂxÍ5²§ g Àpœ|æ|Ÿq“8>^aAÏÞ[q(ä9NDž�‡°Gç§>XÚ~K@+@*ÀÍØÂͲ­�1�1®PâNi�K$¼é<ù`®5nmêh)|jlÎ\�Œ#%�J .—¼+òäD™'‚ ÒV8 Ïø€éÓžþ´Ræj @ÊùÒGî-ØHM¾X×tž¶moš®¡V«5\M½Ë¿ÜWuüñ·qó¨Üê°~ÿXöÇ>µ¡ÇAãq¯¦ÂýÛœeÛ+zlÇÓ-¾uGÓñø�ø­�ÝÑñÙ‚.¿_«ê6†zü>ß9Žß“[z�TŒ© Ðô*Ù4¿-r-ãçvw‚Õ°š.ZXÕñ~E`êõólªYãZUÝà*¾øË‡[mUòŸ$’ƒ`%¯ˆk¤0¨\%.•ýü¸;� ̨‰÷(ö�� �IDAT \„íä�nÈ�>+ǹ>ÚO€(é[ŽÓ½?üaéGŸr·€R„çýD¾±kCûÆá5ØqÀ<}7J˜hÊB8+9VàÊø¬””8¿Ï>û¸T®‘geœÂž%¹*Ðe<r»~ðý@ÔŽ÷Ü+ó¦Qç*îy‚Ö„i~ÇV2C¹íM­¦$º}«†Á~@åÎ HÆïõG²é~iS™Ü>öqëæ€…šŽo~G­úšæeun<Þ1U5¸Ü1cêÕw¯ß½Ts5½—+Ú~¦÷Žãïa7÷´©fÕVK#àŠ� � û Ù ë.(’Ä Œ¸A‡)9XÎç2ÉGâZ »qÄÔ„ÿ¸OÀŒ+%b¯>Ç€”p¡ÏŒÅùŽ;îT$Æ ê°…“«ÿ„åM�ŸX•'\'´è¸bU"à@±U�9ž„’ГsfkáÉ›o¾¹¬&´2И׭[W½éMo*ï…�š^€Ó}t�Ъ±š'/еfÞÕ˜šQÛMÔIò½¾h»ia«ÕQi]rz«ÕZÓ ¬4ÙœšºWS•û²a”/ì©R犼vÝ9M_£-ŠX7MNÁºQþߘ-;O…zýÜ›¨é”›ó»YQÛª]#àŠ*� ÷G‚6'GþÐqŒs$\÷¾°¸>ÜpÀ„Ä@¨�]!:ÏàXh;©¨E¥-× G7îŽrœ$ý9f¿?î0HÜŸ¨¿¶�˜ö�ë:G*’ÊA\”‘0É÷œ%}j˸À™v�’9 %ÚpZŸÂŸVʯ2oP¦ cr�vAÕ�XB¦`Òuæ)tàôç<ã¨êÒ Xcj2¾$úþ½GX¯é—ýÂ:dÙî¦5 ·çMöøxvûŸªÊ퓵!s¿}�õe6Qå6úÕº>~V˧xLú€Ð‰üÎvXÛÜùwû»ÖK›[U}V…«npUÕõ—�‰/~îØ�Ü)îÇGHô€Ÿ�Šªæ/ð!JXÍ^„Bj â˜cŽ)µªÀZ„ºãré?ª­{¶JÏgr ô ‚„#”gLÜ&×U­VéWâ¹0Ëõ Ç88HÆÎ92~ùSQ>Á|¿ö÷_+% ÔÜÒ6— L-îšñ �Gýp¥Ž=öØâ´�y晥ž—$ø(*Ç °‚±ë<2Žœù¤úÖDïB¿¡A%ïª[Èn öìTϤø VÓ{½x —Å/�†ÐÒ)}þœ7ö‘¨>Qºs ˆiÓÒ)S¿}MÔ?Öζ0Sm.°Œø»6†Æ{¿gÅv7�W�ƒóF„ÿ€È&äÈ€¢+®¸¢¬~s>¸àǪ:NÀð|ÐAP“�äYxX�®¸âv9~¸Pª¸ ÁéD…[Æ}â0iPÉÛ1ÀNàí¿¿ó�ÒÎsŸûÜê�C)Ðe«ðdNÚân# fn�.œ­»ÿåîCÜ,+ýŒÛy@¤ ê“[§_¹Vænå¤6åjÙÈš¸~�Oû’ÙA$ØRT+¬Ê½ûR�gÔ+·¤©|9ï^—kè÷K§knB½¯àTüÑØ}”z]­ñ¸Wý†57Wj€™*ÀZÚð^LÅøÖ7„¸Øh2Çóîçwtíü?³¾O¸tmî?:ç7¬¦é¡Y Á#પ72æüX%Rä-pb„ÌÀ*�N�ÈÕW_]`†c6€ ×\øœq¤„ …99€ ÈÈ'œ%çz9W2rHᨠp‰œ)Ç� 4‡È©†r�8N>l~Ô�óm“œ5•¹�:Iê®DrÊ8Xr°´-y? �î½÷Þe›ó2_s9î¸ãʵîBÔ¦±ÊÉJõ§:$¶¹€µ¾®Ø_4ýþ‘ï WÕ/ƵtÁ¯Së남S]ÑxuÃ/ãS˜Ü¾¡® 6™°×Õ}Œf&ºO§;6™÷l]=ß&ð¿|3BZkë{�úÿnáæ¨iÎäx~‡f�kUuƒ+N P°G°á¾Dé�>À(â.¥X§¤Ák^óš*, ‚‹¤=0�¸U`J¸ŒÓÄIâp�±X5Lœ+/É9®—÷›3K¤×N¬H`*œ%ÇA�¾•`�d€ã|óàN�@Ç@”�"H{Ö³žUÚãX�& ÎÀ� ðú•Œoói«$9qÆ 8õ˜Œ•»¥/׸7\1¡PŸÉãÔÊ*Ý«¾¥ðfƒ!·kC]\t÷ŽÕv}ÃÕh•Ñëqí4�aB0µ´ït­°j:—©.츼m{¡‰ {­®AasÃ�ñ¥·t‚¶ú˜¨ß¯åø{º®þq<aÇ�õ}nòE½¡î+«‡kýfº¥MCƒ£ÕW릉γÛbÔÚ´iÓ}UUÍk0àÌ�"IÙÀcõêÕœ€Çô8ç%/yÉpiûþ+€#¤§Lƒó½V èÈO*\ùYò§”2FEÀp 5rÌÀ§ö«ëUy~’Ì�Š>�±s†´ ֜DžLò¡�h“0}‘9)Ž:÷s«›oº¹$¼%«�…)%à;ˆÅ*ÅE‹•Ü,cμôÒKK%wýÈé2÷•+W–×_|qH— ¼q½¸k}”a¸¿Õj=fÄÑY¬V«µ¸Nïµ§U,�¿sª�Ÿ¶±U}«Þ@¸µ)Ó��ýü¶Åý”‰h/Î9ÞpGÅxÆÚ&¤=,ÛouìñªÉïi{)Љü�ß¶¥RçÿÇÛ\ÔYé~4Ô²¶ßý^ÐÔþsïïýÝcü®´«ßœÅ�Óˆ í¾èA޲ÜPùÀ…KÃ%R#Š‹D`BHLÙ‚3Î8£l˜�°‘ï$WŠ3$´¶Ë.»”ºWœ#«í< A*âÉÙfÔwÔ�>B…Q§*J%Ä*=�å³]wݵŒ?r±À™Ðøò%¢‚<øZ‘  ¹Zœ8ð&A›œ2a?0ä˜g.•þb áJã²2 r£ôã3ÉearÄ.»ì²b ”s›T§š«¦©N*ïKmc«fë–iPg®Ød…jÇ«™ø{0~Oãç–ÿŸl¾â “y/›îl0kŒ úÒ'�+쬴ÚMM²;8B\(!=0¢4§K.Ó’%K `q‰p€-�Ž>üáàrŽ2�ƒÃ# q…�Œ¶…ATÔ©{@-ö 4 ÅiGÆXÕµºÀŸcVö%m¯hÈ)“�À<% ¨}ðUÕ9eVºÖ½�{)Nï~÷»KŸÂœ¶¾±ºXEy‡Ë/¿¼\',â�§q¹' ,J1¤R©T*5ƒµ áâ•YWÛª]#àŠCsî¹ç–¼ á+ Å•PÂqÜ. ˜JBlÂ[ò�¬.´NÂxl�#¤’¸]ÀF{>—£¤Àèûßÿþâ<(IóÜ&ïø ó™ö@‰g ÂÑrXŠúYÆŒ\h\ h�˜\.ã@rÊ¢ÖH2N.—|-Àì@ ~�¤�ÆHª2´¯¢q€PýZ¨/(æ4÷ LqÐÀ\‚U*•J¥¦YcíÛÛ õ 7vÓ¬å> ®|Ñst€˜!àB¸�k$,¨´‚„vå¸G� l9KÂqç�w^ õq�@•p™ZQàBý+Î °Ðæá‡^®×·öAøò^œ"íp¢@R;HE}*À¢_àÆyãníø„Ë\H{à ”q×ì(<§?y\` 4ÉÃMηrè�­ÓO?½„…ÍSB>8Ò/¨’we<`Ëu–Qœô¤“N*}é_î�†œ?årÛ›T*•JM³bK¦5]¶eÇ^¢³>On„sPN=õÔr£¸/` ÈOBj7Å^��ä]ïzWqs@Íõ×__ ƒ{Ã9â„ /‚eÀ†‰} íZr¦ô+I.–ÄtЂÀ�)�‹´¼�˜q»Äyðô£�?* îJJ8Îuã2 ãÙ'0r·8T È­v”xn^ÆcÓe�gÜÆ*ßÊ88^úÞÔ—w …�Ýý�ZrÓ� §Î=”[¦Mר&(�«T*•JM³&zUò¬Ï¡{XB;§Ç—=�°:NO¾� \(!3‰æà �.w lX=('ƽ[›Ø®f�=ö(uÚi§•<%à€„A ‡ x8/ �‚'°ã=`Túæ&�$Ð!DnšþaÜ7ã�~ÎçR�-N“såO9.÷˹® ˜†ú^VFêß\$¤ “šGNåæY5¬„<½¦á´£Ü2¡IsŠ*ø X©T*•šFm¬MK3tSûJÜY«açªý ^~XðÅP€7FXN P"'Š##L0„Ƹ@`ä}ï{_q›8GÂt\+®(l@E[Ñ�*á?îÀâzUuîÐ6ÎåZ�  Ç=òÄ€.cÔ‰cdKÇ$Ì/]ºt¸ø)GŒÃ¤m¡I!BýsÙ�VBvò«Ì\rˬ`HœµØ–Ç¼ä™ ïí»ï¾e, iýúõå~7EDA¥1ƒ}�ϵêdeX0•J¥R3@å^Í´•ºÓ¢­O;í´•UUÍ­ê2 $xHNT @®�gŽ‘ æBh` @ɱâ^�î �q.¨p¾ó8a`†ÓÌœ@¸LVãq±¢ 0#�‹+.Oä^:Iè7Î7I¡Nî‘ó‘ö¸nÎwœ[üô ô€Ç üIÜmœ4ãHáÚqë��3om›sb ¶|¦”C„;�è²:¬YUUsµ}�ÖC­Vë#ަR›©·¾õ­y S©TU×_ÛÜBÀÂ�5âè,T�«V«5—#ä ¬îãö(#À� `Ãçûï¿� �!<Èá‰pБÇ8X@BÞ–PÈnœ#îÔ�7ÞXò·€¸àäœþù%¯I.w¨IþŠ/ú‰Z[€Ç˜ŒO"}¬.ª‹U`ʱ(à œÈ]BƒÚâ<™‹±i‚!ç€Á(AB�ú<çœs š§6�§Ì„Vª±›;(ãŽÃ_ýÕ_•såh�9󌰤q:ÞGX0á*5)J¸J¥Rµþ¤­¨ìx$xbÞÌ_¨ä\�›ØF†@ÈàÖÄ&ÎÊ \tÑEÃÐp„ê$´üñ%qÛqyW��[µlÙ²%@BHŽ;&l›Ê>þñ�â�pã ®·+B�ÀË1nx±1\ÆAâˆ1N”Ïœo^ÆÁÙLú0/Д€$Psœû´¬�”‡”þ@Ð\PÂ�öPT�]H2cådYèsí¯X±¢„X¹núŽª{ "S©T*•šoXpC Ì*úm r•ÀH�IÜa,Ðb¸.Bj�‡Û˜8IÂ�r¨€�s�O9 B>çJèŽk qÍ5ה׎éèp“¬¨Ó¸#î“¶¹YŽk_~@y�Ï#œ,!@cŒÆ ~¬4$ ¤ä§ (£-tŒ2Žœq™cìMîÈHÒ¡ÆâzÀæüÈ5ãhEAUÅI…µãz!HûP:W©IQ:W©TªÖOkPŠÊý\¬Gö¸9@êÒ: È­úƈ3f¹Fì-È!âú€05¯ã& ­9pq£€ Èàö8€ÇŒ�$ Œ€Œsl�c;‰ê MB…J$�&å8MêMIW�”ë5¤¢¨(Uú]r €•>Ø1 Ëq•¢¥ö8à€Ò·¤x°tŒM»œ;€g|J;¸Æê€pó™þ" äÙ[�“eœÆ,œé<¡E‰ÿÜ5sõ|È!‡—+œ·1V æÞ‚©T*•h�ò¸Åéa íQD”óÂÕá¼(¥à=§Çj:ã!Áp�"pânÐ�&Ê‹Ü*Çä?BfQLTƒ®B“×¥ä�:P€(6i.ÎñÐHâHy„ûUÕùQ€F¿�ˆ›˜@@RÁØœo¥sL5í{Ö§ žx≌ôë3‰ò>3.�fnú2'Ы$�9úw\¿Â¢æg|1æ1~©Ò¹J¥R©Ô@k�œôa¸Š/w€PÕ¡8�€€�!3Ž�œ#€#)ý+_ùJ '%€&¯ˆSåsa1`«�@±âPä…à„!A‡ŒëÄÉ’T²äMq¯@wIÿr–„øl/V´Í52`$ÿ‹ó€"Œ) Ày�º(;¡_çWìohÜ Iÿ\2ï½XÆ&ì2åž %rÑÜ7ÏÊ;�7} ÇO¾7 œ:Þ@ W©T*•h $\Åp! Æ âàß]yå•%W @� Œp˜ó¢´· �€ B¹=àIΑœ$Tì¨n�% HHœ)Óàsý;jÛ{N—‡S&Ñ\r˜¬òbÚDÆ <¢¸]·ß~ûp)%„è�˜:SV1#wŽ“%lž@�þ…;�™óÍ€»È½2&e„ £ ð‘ /iîŽsÄ.¹ä’²uPlC÷©„«T*•J ´®b>á-á2ùE±ý è�€ˆ@‹­mÏäô€)‰íÀH}à(çs�¢ÇøJlçîpÆ�(HÚ4ÁAŽR€ Ðp�ä83Ù›ßüæ1ú÷™ó$ªK<W¯Ëø¹d>Ó6 Ü) x.¼è¢ê€ý÷/ã2Oã4~àEœÀŠ“ÒŒQvIø  t2ÀæÙÜô¡àglÀÏ J9W •p•J¥R©�Ö ÁU×¥j ‚k¥\‚R’À=ôÐêä“O.îˆâJq—¼dB`àXå!8�ÆéR®á¯ÿú¯Ë9`͵€˜ÉçL€Î1À"O)ª°Û Ç1¥ª�ÁÈM\©¨mÒ8U{íµW�DŽÔªU« ôØ+PH.ÊC�-}Á]žùÌR«j·Ýv+ýrÖÌA{Þ7¹ÆÊ|vá…'ÃC!Is1&ï]«Íç?ÿùeAЖ•ÙS©T*•L�p®ª:4¸ÝüíJ¸MŽ�<*ùU\NŒœ+�ÂQ'Q"AO¨,jd�°q '·è/ÿò/ ()úiÿ@n”ó\Ï-f$Ðdká8î�0ž~…¹Wò±À�ã ¹Ž;¥N¨â®Dî”y�/P¼Œt í…Ë%Üèu"¾“kflÀLr¾pdÔÍeÆ&¿Ì=r·*6˜Ö—6¹aœ+[ñp´#�½Sé\¥R©Tj 5ÐaÁª^¹EÁH�:Ê p¬TRçÜp�À‡‡›¤„_ùÊW‡çÌ3Ï,@ó¾°Z½zu�/¤mî˜÷ÚðPKŠÃ„À—Ï£*<X?ç� 8‰¥q%IéÀÍ~…Æ’„ �M‚9`ãVÅ3÷ €�.ÅKãD¹†%ydjOÛÆ&Ýû GʵR<õõ¯}Y]éP¤€ÀÒ·û"wM>(3÷­�®R©T*5Кp%œÅ•â † Ji†öÍ–Á�ó +ÂÂcÖ…]+„Æéœ!n—dp绞C „ %ÉK&Rœ&Žàã|IFºv³‚(ɹ=\+.À4`JˆQ’:GÊ<ŒgÌüœÖ@0âÜ�,û'.Z´¨\oÌvòˬ ‘ŸøÄ'Šk¥,@çM8ÕêBãZ>ÓNU×·£ÆU•p•J¥R©A× ÁÕV#ŽÔòeï””�=Q ¸ÄV4Ü$ð¤ä�Ð!è° Xâ¹X9ÎÓ†rŸúÔ§ ð¥)Ó Ü&<˜@ àˆ� c?AcRÀGß� °hÜÈ•f®M�QžA\/ÀC‘¼.\È)sŒÃæ˜</ÐgµŸâ§áœy“rÑ„ ßûÞ÷ÐRóËJD!K ©X©0!`ÓŽë¢ÜEû½N¥R©T*5ê W#‰ —A–pâ" á�#ŸYqÇi,¯xÅ+ ØHlV@+6Pã6 ·q ¸J�D>—½ 9OŽs‹ôXÔ²jã2©+Å ÒŽ„yããt CÊi7 Œ›¦cæ%«ú@!øs¾ñ¨…Ð8\Q6Á¹ ° g?�ŠM©�¬n\¹re'WË}r­9põ„A×kݺuåáAPÚ ±©T*•J¥C]Âñ¥Ïé \ðãX’ouÅW(Š�‘¹2 ‡«$Œ'l†�Ê1ÇSBh*¤ÛŽ´íËu²²ˆ�:n' °y$’ᣄp#p‘ÿxô/Ì”ŒAr¹ñ¹èqÙäBqÜ¢Lƒ~ ’¦ÄQ2_í’~’î]o1¡Pû%rÔÌÝjJy[rÅÎ:ë¬nQ ÕX]gnî#¨ÔfÃÕ‚L¥R©Ô@kàs®€�÷p¨Àpáú€'Ü*@% ÜDò·r\.¡;%"/ 09‡“ŒÀа°áDéWž§ 4ù|¿ýö+cÑÒ°q.W ˆù ü7Æeâh�AŸ+Õ`œ)¡9u©$À[­(¤Éy‹Í©A£ùªün. Ñøµc¾Ü+aEs�Kµ|ùò>k Ï9QdÔu Ë^ƒ\8àen •p•J¥R©�Ö@Ã@œ@㙲Ô�â�EUô¨â €V8Rž9P®4Ÿþô§‹CeE X ià*Bz`G›O^ð‹�Ž…â¸=�ŠKä<ûê[Ò9hÒ—Ü+ãt>˜‰’\(É÷B… P^–P¢ù8ÿ]ïzW©‰%OŠc¾Ì‰ËeÜÂxÚVþF© ÷ÉÜŒ;J@˜“ñüEøLÊ1‰ /á*•J¥R©‡kà�+�áQÕ«ÙÀ�(�-Âq»„' { ��*¯�@2ÞÕ› FÕvס%ÂdÑA\Ô˜úЇ>T@y(ÁeR&ÔI€9.<§}!AmIš×‡p¦‡¶9T` @àc>B…�ÌØEå¸É¡âÐ9¥"´k®’ê� ‚O盟6?ó™Ï”•†VI:ǽQ•]ÈÐõ€.á*•J¥R©‡kàáª]î�gîÀGqDœ~ðƒ%I: ±jŽËtì±ÇV×]w] ÷ ³)W d8 à <qz¸<k×®-�´ô'<',ÇIaœ$N‘[œ¤p¶�•Ïåx¹Æqãq�>„•-x´…PÍÇkð³fÍšâ¢I¡I�dU# SÖAˆ€>Θù¹ÂB—‚œ;!AsdΓÏ%o‹c§?�¦?ãn¨„«T*•J ´ ®Z›6mº¯ªªy#>é<±.� Ä%1ý�C©Þò–·`�OÄ­!Ÿ 0|�‘Кpœ<% $ÜÆÅkœ(+ó€•\(çp|8eQõ �íq½ä@]}õÕ%ÿ*r¡ä;§LÈPŸò¨@’°¢ÐŸvmöìî“¶À_ÔÎržÊðÆò¸l^wJ�®HŠ4W!Q�I¾˜}ª=í(QÁÙzÎGBûý­Vë1#ަR©T*5 ¤•ó=K1´+@�Lpi8/W^yeIæ¶R`©ås¹Kœ%0‚ä�tÒI¶¸KB†r�Ž<òÈ’ô„œ'1(q²„çœï��%.@:œ#¡A+ü¬ÆãHéÛ8= r®n¾ùæâ’q‹€ bœ)çê›{vØa‡U÷üë=%^ÕgœQúóƒv¾+p¢ÔÂÒ¦9r¹.¸à‚Ò.× ¤›ûcŽî�{£ÆU�§ù9?•J¥R©Ô`j̰`UÓ$°àʃyÈk’“<@çGÎèB�@Ãq!7õ§@Ç pTµËÃM"á7+å= §q‹À‡Jîío{IjW?K“²`<q¾äjqÐÀšc ˆKêäX»0¡•�úkßüÆ7 à�$®“\*9Zûï¿)~j¾@Ѷ7�R— Ä)/¡Oη ˜q輆úq�~„.¹qîQeX0•J¥R­� F]«nòY8EöøPr¨„Ç„ÕÒê’7%”^„Á"ß$q“€˜šOàBÈ ˜8�cl|°ä6�2ãá ½INW”k¤ß(¥`Lr«8CúGܨöJòÆ èÆ)O Ü€&€ÂôÅÑâTq¥Ì ¸Éá’û%„(‡êì³Ï.Éíòª@ êìÀ üDn\lh¯� @ºBœB’½îueX0•J¥R­A çjhhh®/ÿÈêTlä Ž„à¸PÊÈ]âØ7Ú�$Ü›o¼±Ô‰Š ïÏ{ÞóŠkŽV@Gˆ‘«è€pCÜ.ý�ð"¯Êk°ÅÓ×H›r¹\cŒ±Ù³+m;fÌ•V ‚ŸH¨÷ÞX8_@I;€œ�C¨ã>q¼P�$uyXàÎxlè %Ïãá‡^Úç¼§<2ðØë>�¢t®R©T*5иՂ­Vk.˜ pÒM@BÒ¸ÐqŠöÜsÏ)àã¸ãŽ+àóÉO~²äS�"¡4a8p!7 P §iKˆ�@à°—ŸÏ¹R\,!7®O”u�)¶¼q­ð¸fܦØÒ†ó%T d�\.íAç©Ì|¬P4 ã8K» Îq×�9Ú›Þô¦Ò—•ŠÜ+°'\iN(Ÿ?÷¹Ï­>øàâÜ�(Iíæu±Ü€赊çNZï²%NÂU*•J¥Z W  Ë—ú°@Hl¨L Â{á3eå É?jÚyík_;ìH�0ðdßAÍü[��ÖIDATa8¡6mÚŸÔp€@Ò.x`œ1®šk8Köt�×Ü2רPØ q°$Â;Ÿ¥,Àz”+… Bj_ITçTÅ–>úŽqË�’Ëà\ï|Ð¥LßýٟЂÜ:²BR"~$º 0l�áÀn÷»Kˆ6á*•J¥R­�ʹzX)†^¹@Ý �80ÂYú“›ÛÃH‚NSDTr»äp�㽕| m‚n•p[$¿ƒ&®‡(B�\°p~À ·ÉyBœ(¹O�I>Ø’%K ô-×ÉÕZ¿~}éõÆ%Œ—S¥=uº¬ ä–�E}EN8âŒEM-ÇäYqé$ò[�Ž`«ê𦾴{ŽùiµJŸÀL™ŠúšÌ¹J¥R©Ô@«gl©Úú-oyˈíoº=¸:@ˆ›ã5w `��¸ÎgΠȪ>•Ò��wÞy%' � ³qw¸=œ í€,@ĭ⌠ç­Zµª|&Ì&d'|á= ãà HP’?L¸h€F(ÌIRnÎåD—ªoêG>•$}׃<ã3ïßð†7wí¦›n*aA×r½À”„{ hlÆíDåö¸'Õ¿4펡gÈÁâ¬]¬t®R©T*5Ð$çjް×IØ q]@ITJ?rž@À�=`ÇyQ¹½]VÂù €…{%dÇÙ9úè£Kâ·’ ‘x.„&O 0Ƀ>VÆ&Éœ.!<9N*¾Ës26�GÚ‰mzÀÛÇ>ö±’LnNÆî¶8NÆk~� ̯çK/½´\/¼¶ŒIH rØÔ´\  *ñ›Sƒ+}™³{¢?}¸WU½�P7תKø¯ÈXA`lò<ŽøT*•J¥RÓ¤Ö·¾õ­û¶Új«y€�@Ž\&_ò@Fr:ç €1Žl€$.V7Ð0�Åç>sÝ™gžY ‡ûóùϾÀ±ªO‚<÷GAR€bLó·ß¾ÚöQ�*c’Û¤ §ÐWIÿ LxR>—úS@©ªÃ›ÀÉœôýñ�¼„�ó¶·½­ÔÉRKò9PÀ LJN7 âÀd6�jðÎwä:P TA‘ûºªŽPkÜ#Çœ ¼äni“{Fæ¯=ýyrÈ!÷Ï™3'©T*•X�áÙÒÔúÉO~rß<0H�á(ùQ¾ðÁ‡P& �¹G �H,'�¬Š“ÅqU®óÞ64 …“¥N”óm£#lj€Œp¡P`9묳JûÊ7È¡:çœs Hiƒ ¦ P$ÏŠ«f€¡ë%Ç †ËÆ@`T&B-uªÀ�² ±' ±‚,�W½êUå�3f,ú'€åµ°(x‚ú¢±áuUW¬® ¹Œðª+÷žCæçÀ-ã\<szík_{ÿܹs®R©T*5°¨œ«m·Ývå]wÝ5W"8øàY'¼T8,À<��"Çå"1u¬|&ÜD8PÞ»”p¯bPE ŒÅ ‹êìFÀEû>—ˆ®¼C8DG �£È�‘R@H5tîàїב×åÙùæ'̨].œ÷ö‰0ãª4�øÌ,jÃÀ”_c×W8Z <*ï}mz8ܼƒÎõ,y^nØ´R?VU^sÍ5Åa{êSŸúPUU™s•J¥R©�Õ@­<ꨣîÛi§�æq�¢j9h0�ðÝ.Ÿƒ ¹PÂ}Â`` p˜œ+t®@È�9`Âs�ÈðØ�áÁ—¿üåe@Ê'8‡��dx9P¡(ÈÉmâH£ñÆö<\2«ý�”Ð£×Æe^Û>zÛê�û(Î8´/ äu€c¼ÚÒ¿¤ucç�™¸‹Ð'`Šdv¹]By ÍêÇpò„ø€#¸SîGl»÷FNYͨ +­Z”\ï¡ßýöÛïþ'?ùÉé\¥R©Tj`5PaÁã�?þ¾Ç>ö±ó„¡|ùsl@xà�*yGÂlB|B`¾ø¹/r¨�“'î‘v¸>ÀF¾¸áÎxÍ-ÒŽöä>á9nQTm"‚SJAâ¸0$XÑ—ó�¤€Œ&À<ÆrÔÛ⾩melÜ&ãzƒ Î8ÌÁg JxѼ�Ëx´gLú�ˆ ø�/ã÷ ê�qß\ÉÓ6Iêw�€”qsÿÜ׸Þ5€Ë1î˜rÀQ™ ã¬óÓîßqÇ®R©T*5°(¸:á„î{Üã7Pùrçà�_ö`‚k+섽<®Hê$À t0ôõ¯½€–s´Å¡òàò�¯Ï}îsåý~ûíW`ÅkáD¹ROyêSª»¾~WÉg¢�~ô£Ã›;ƒ#mr}„��ö�páq¯€ · |¹Î9� $ 9 M‚c1B\)–1h/Ü/óú`ò´¬|D‘¤.·Ë˜¸làDE¸Qò<°bæ#AßžŠî-°ˆÊ.pÌ”š¸ëwU÷þðÞâ^½úÕ¯nÿ9Ý_UUÂU*•J¥V•sµÃ;¬üñ�\*´ ‚ _þÀÃDUP�07|q]€ˆÐZëÂiÚ�%6mæ"q¦„×"çü8Ïv9`ˆƒàôÈ"t&éü /¬Ž:ê¨êÐC-�ä3�c `G_ H`‹ÓœÀhV±=ŽþA‡ë°Ã+0´nݺҞÜ.®(3oð°<\« Ç•sHäŽEõvã5&Ÿ&}<€E€ÏfÓ®W l ¹Ê sO¿ÿ½ï—±6—y�Þ»ï¾û¡vØ!s®R©T*5°¨íožóœç¬Üc�=æJS�ÈÖ/¾ìA'†ËâÁ±XÀX�Å3Õ„’Ä+¨äiy AÞ•×\#.™äm¥œ°¸^ Ѐ!;N�<+Ð#¯ ÷y\<sд b\ü„"Á�yè�ë%¹Üö6ÀÇø¡úY€H;œ)ý)Û 9_é÷Äø\ м–#¦ m{/ä À8c Ï<õó9ÈuÆ…E�•sÍÓØ �U‡Ü2÷ŒÞsÏ=-Z´(á*•J¥R«�Jh?÷Üsï[ºt鼨o/J€ŽLl~ ß\MU'Ÿ+�hœ£,hˆ²€MUs0ĉn˹cµ#3œüÎ-zÍk^SÞ /‚ð#Ñ´X%¨áCð'œh¬Ü#N‘p ,áÂÉ’lòTiW¿ÊyB˜B—®‹ü(ç�D�'4( $��¼-�ÇYó��` ¼Ýxã�¥>—k„ø�ž~Íל´kNî§Ò `NÿÆ)©_»±…�ûëþ0.×ÕW_]îß;ìpÿ=÷Ü“aÁT*•J ¬*,¸óÎ;¯\°`ÁÜ«®ºª|±s¨�÷E)ε×^[`ƒK€�E€Áª=!,Ð%¹Ü³�šs¹6Î^\°!$geœk¼p¥ „õ@pÑ'‡£åæÇ¬°3f�Ž�~9WŽÉc~€L¸ ,;׃#}™ƒk•<�=Ü7Ç9I Ô�8Ð6¹eB�@ @:|™›kŒ+НrúÜ'°ÒÌÏØô/ŸÍ{÷Yâ>÷KèÕô§máAý=ðÀýùŸÿy:W©T*•X Ôö7þt„óäNÉ+úò—¿\€4qi�ƒ/|Àrn¸á†9€�©ÇÄyqN¸/Êp„�”vŸsj¸PÜ"ðÀ™âèD®’ðŸ1p­„ÄöÚk¯ZúäDqÑäx Ïq~€�„z!;@¨_ç8L��øÃ¥m®ÜÁ\@NûV"ðhl ‹8sæa`Šë%il`Óý�™ò»€�~8t΄±'b䯙3@2¦¸7Æ,×J¾™s…}f®æ"]Ã%Þt¿R©T*•JmÚzÍš5+¿úկΑÈÎqá0…³ã=ÐS€ ùâ'¡=.wÆ9à�g`€!ç�"9Eœ$á7 …:õɹ9`,q–8WÎNM\a4mƒ�ÈïŠý÷\gún×_ýp½*ð"$è=èt>çÐ8ÎQ2¯}öÙ§´AÎѾ¾8IÆ#wJÞ(tŸÈüäU¤Å‹—{wî¹ç–ÏÌÓx�ÃøŒ•Ь˜äX¹Ï\·ØOÑ=v/ô5oÞ¼‡;ì°t®R©T*5°(çJBµ�Ÿ¤oàÀ}±ŠÈ€ǸKà (/‡ÇuÖh—(w�\€(;À €€ ®qf€ŒkÁ¸qàá � Ù§ñ8ÆAn Ïñ¨žN8TŽ Íi7JJX¥x\Ë]R;ëÖ[o-×mr·|&‰ìp·´ ü¸OÚ䨙¯1šÀÒ‡±È¥ŠÊí6˜Ž:ZÆ®”ùÉ';ðÀË=òzÚ�Êó±½Ðsß$àsS©T*•JmÚz›m¶Y¹ÕV[ÍÞpR$“{ørç¬x €)J+€,!9®“pgÆg’Ý…³@€’xJ¸6Î &—^zi¹†3Å•6€ ì(‘�Ü�GÇØ.»ì²2 Ï�’q�ÇÁ :S�Pû�Ohißø�’ϹNúE€2j}�šî (r­¾ÌYûÆ#´€€(Ђ0÷JŽ•B¦€�s¥OýèÓ½6_×È·z¥Ü1!Cý,Θ\µ|ð¡ÝvÛ-�«T*•J ¬ªÃn»í¶òQ�zÔ\��”€Ž/zn ×ExJ8Í>§JÝ(À2€�`À‚¨¸EB„@‡\gÅ ÚNÞÁH”}�ÏÄ-Šâ£(ðEÀðpÅ\£]ãã:�4a?î—Çç\&+ 9Uœ+ã®°1V Ç­ºâŠ+†C‹ Ð|ÍŸKáPn�± Í4 g~`Ï9�PÀÑÜ7÷ÇÊA÷ËyQœÔÜÌÄŸU“ Í}ŠÂ§ ÎX¼v?ÝnÚ~ðƒ‡:è „«T*•J ¬ªé§žzßvÛm7�‹Â�áö�Ž€'�àLµï T€ ˜àá;Ÿ›()ÀíÑÇFû±?_$Í+( "¬VG}tÉmŠÚS\,.�¶¸8ïxÇ; ôØh�+÷Œ€þbËùK\­÷¼ç=Õ%—\RƪÀTC …Bv(ý#hô¹±Ê)“‡uÛm· î—K› žjÆÖ6€ÑýÒ—×`1\*áPó焹¯Ü7÷�Óæ÷äÛßþöýgŸ}v–bH¥R©ÔÀj�J1ÌP¹?àLÈE¾Ø §'rœ89€4€ 7ƒóD€8VÇi[Ø�¬(<Ë–-+íØâ<€ aÆ“O>¹@ð�KÎÑ&Çì,Y²¤:õÔS˃Þ/`#é[»‘ h8fá~éßØÍ‘3doAyM®‹ ¡9O ÌØÔÏr¢j< <ᄊ›dims5V÷BG˼]”œÏ¹âv�±Fùós­9r欌Ô^”k�äî.\ì÷˜J¥R©TjËPkÅŠ÷=ôÐCóžýœgW»î²k`•ÉåO�a=_ô\¹@à*öõ6Âb^¯PÔ|r-¸xË[ÞR-_¾ÜFÄÂbµHóзÏÞð†7 ùíÝÁmÂ@…aç„„Di€R¸sàFé€S¸SÔ”BŽ>+YÆÉ1Ö< Æž�EHüz³žårÇ ¤l·Û.'0$'Nš8”×(û’äiNÇ㱃89šW(ÙØŒŠÏ9GünÿÀ·¦9œ;× 9f¾b’r#ÉEÙ@˜›Ãì@¡5b@ÒâzΖí}¬CSFäj‰!6ðä*•úN‹Åýp8”sU*•J¥ÙjV7ßn·¯Íf³ò'>¥4ñi¹�V²Á³k·<Û½p��@â~‘ÞWÎÕÜÓ9âyïàa=”ò òs-PsàŠ@ˆ2]\§¿ �Ê+/“q”!Î{e?Ðç({æÐÿ!�)±å§”i­ZàÑ>‰×ëõ±£µWœ1qs·£rªõV+ß©±8]´\.ï§Ó©àªT*•J³Õ¬àªý¡ p§èñao¢Ê\Þƒ&0ĹW�2a”Ð�€ÂZß)êƒ�× †S$�8ôÄr<ë—ÈkÐá\±‡{˜ÒÞa¨ŒÝ? dÎÆæk¬aÞY /¯ôáû®¢�Ènî äFq³,ìw·¡yº!@þ\,p•GM[¥ÑËåò<@©T*•J3ÑØÿè¸úlÛvõ*ÿ©I÷ácì³±C¸J¨”ð†qs¾ç� 8š÷7¹��—~Rr�W€Ë# >Óf!`•¼¦Æ;îz ì9�Ö¹qáì!¨Ô¹Ûíº1@"ˆånqìÖëõ}¿ß¿?+•J¥Ri&š \5Mó µÖ�•wE¢����IEND®B`‚�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������pallets-eco-flask-security-72bf929/docs/_static/openapi_view.html�����������������������������������0000664�0000000�0000000�00000001520�15257217435�0025175�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ <!doctype html> <!-- Important: must specify --> <html lang="en"> <head> <meta charset="utf-8"><!-- Important: rapi-doc uses utf8 charecters --> <meta name="description" content="Flask-Security API"> <meta name="keywords" content="JSON, Flask-Security, API"> <title>API logo pallets-eco-flask-security-72bf929/docs/api.rst000066400000000000000000000346111525721743500215060ustar00rootroot00000000000000API === The external (json/form) API is described `here`_ .. _here: _static/openapi_view.html Core ---- .. autoclass:: flask_security.Security :members: .. data:: flask_security.current_user A proxy for the current user. Protecting Views ---------------- All Flask-Security decorators are compatible with Flask's async implementation. This is accomplished by wrapping function calls with flask.ensure_async(). Please see `Flask async`_. .. autofunction:: flask_security.anonymous_user_required .. autofunction:: flask_security.http_auth_required .. autofunction:: flask_security.auth_token_required .. autofunction:: flask_security.auth_required .. autofunction:: flask_security.roles_required .. autofunction:: flask_security.roles_accepted .. autofunction:: flask_security.permissions_required .. autofunction:: flask_security.permissions_accepted .. autofunction:: flask_security.unauth_csrf .. autofunction:: flask_security.handle_csrf User Object Helpers ------------------- .. autoclass:: flask_security.UserMixin :members: .. autoclass:: flask_security.RoleMixin :members: .. autoclass:: flask_security.WebAuthnMixin :members: .. autoclass:: flask_security.RefreshTrackerMixin :members: Datastores ---------- .. autoclass:: flask_security.UserDatastore :members: :exclude-members: create_webauthn, find_user_from_webauthn, mf_set_recovery_codes, mf_delete_recovery_code, set_webauthn_user_handle, us_get_totp_secrets, us_put_totp_secrets, create_refresh_tracker, .. autoclass:: flask_security.SQLAlchemyUserDatastore :show-inheritance: .. autoclass:: flask_security.FSQLALiteUserDatastore :show-inheritance: .. autoclass:: flask_security.SQLAlchemySessionUserDatastore :show-inheritance: .. autoclass:: flask_security.MongoEngineUserDatastore :show-inheritance: .. autoclass:: flask_security.PeeweeUserDatastore :show-inheritance: .. autoclass:: flask_security.datastore.SQLAlchemyDatastore Internal class implementing DataStore interface. .. autoclass:: flask_security.datastore.MongoEngineDatastore Internal class implementing DataStore interface. .. autoclass:: flask_security.datastore.PeeweeDatastore Internal class implementing DataStore interface. .. class:: User The User model. This must be provided by the application. See :ref:`Models `. .. class:: Role The Role model. This must be provided by the application. See :ref:`Models `. .. class:: WebAuthn The WebAuthn model. This must be provided by the application. See :ref:`Models `. .. class:: FsRefreshTracker The RefreshTracker model. This must be provided by the application. See :ref:`Models `. Packaged Models --------------- .. autoclass:: flask_security.models.fsqla.FsModels :members: .. autoclass:: flask_security.models.sqla.FsModels :members: Utils ----- .. autofunction:: flask_security.input_svn .. autofunction:: flask_security.lookup_identity .. autofunction:: flask_security.login_user .. autofunction:: flask_security.logout_user .. autofunction:: flask_security.check_and_update_authn_fresh .. autofunction:: flask_security.get_hmac .. autofunction:: flask_security.get_request_attr .. autofunction:: flask_security.verify_password .. autofunction:: flask_security.verify_and_update_password .. autofunction:: flask_security.hash_password .. autofunction:: flask_security.admin_change_password .. autofunction:: flask_security.uia_phone_mapper .. autofunction:: flask_security.uia_email_mapper .. autofunction:: flask_security.uia_username_mapper .. autofunction:: flask_security.url_for_security .. autofunction:: flask_security.send_mail .. autofunction:: flask_security.check_and_get_token_status .. autofunction:: flask_security.get_url .. autofunction:: flask_security.password_length_validator .. autofunction:: flask_security.password_complexity_validator .. autofunction:: flask_security.password_breached_validator .. autofunction:: flask_security.pwned .. autofunction:: flask_security.unique_identity_attribute .. autofunction:: flask_security.us_send_security_token .. autofunction:: flask_security.tf_send_security_token .. autoclass:: flask_security.AsaList .. autoclass:: flask_security.SmsSenderBaseClass :members: send_sms .. autoclass:: flask_security.SmsSenderFactory :members: createSender .. py:class:: OauthCbType[oauth: OAuth, token: t.Any] This callback is called when the OAuth redirect happens. It must take the response from the provider and return a tuple of - which will be used to look up the user in the datastore. .. autoclass:: flask_security.OAuthGlue :members: register_provider, register_provider_ext .. autoclass:: flask_security.FsOAuthProvider :members: Extendable Classes ------------------ Each of the following classes can be extended and passed in as part of Security() instantiation. .. autoclass:: flask_security.PhoneUtil :members: :special-members: __init__ .. autoclass:: flask_security.MailUtil :members: :special-members: __init__ .. autoclass:: flask_security.EmailValidateException .. autoclass:: flask_security.PasswordUtil :members: :special-members: __init__ .. autoclass:: flask_security.MfRecoveryCodesUtil :members: :special-members: __init__ .. autoclass:: flask_security.UsernameUtil :members: :special-members: __init__ .. autoclass:: flask_security.WebauthnUtil :members: :special-members: __init__ .. autoclass:: flask_security.Totp :members: get_last_counter, set_last_counter, generate_qrcode Forms ----- .. autoclass:: flask_security.ChangeEmailForm .. autoclass:: flask_security.ChangePasswordForm .. autoclass:: flask_security.ChangeUsernameForm .. autoclass:: flask_security.ConfirmRegisterForm .. autoclass:: flask_security.Form .. autoclass:: flask_security.FormInfo .. autoclass:: flask_security.ForgotPasswordForm .. autoclass:: flask_security.LoginForm .. autoclass:: flask_security.LogoutForm .. autoclass:: flask_security.MfRecoveryCodesForm .. autoclass:: flask_security.MfRecoveryForm .. autoclass:: flask_security.PasswordlessLoginForm .. autoclass:: flask_security.RefreshTokenForm .. autoclass:: flask_security.RegisterForm .. autoclass:: flask_security.RegisterFormV2 .. autoclass:: flask_security.ResetPasswordForm .. autoclass:: flask_security.SendConfirmationForm .. autoclass:: flask_security.TwoFactorRescueForm .. autoclass:: flask_security.TwoFactorSelectForm .. autoclass:: flask_security.TwoFactorSetupForm .. autoclass:: flask_security.TwoFactorVerifyCodeForm .. autoclass:: flask_security.UnifiedSigninForm .. autoclass:: flask_security.UnifiedSigninSetupForm .. autoclass:: flask_security.UnifiedSigninSetupValidateForm .. autoclass:: flask_security.UnifiedVerifyForm .. autoclass:: flask_security.UsernameRecoveryForm .. autoclass:: flask_security.VerifyForm .. autoclass:: flask_security.WebAuthnDeleteForm .. autoclass:: flask_security.WebAuthnRegisterForm .. autoclass:: flask_security.WebAuthnRegisterResponseForm .. autoclass:: flask_security.WebAuthnSigninForm .. autoclass:: flask_security.WebAuthnSigninResponseForm .. autoclass:: flask_security.WebAuthnVerifyForm .. _signals_topic: Signals ------- See the `Flask documentation on signals`_ for information on how to use these signals in your code. All Flask-Security signals are compatible with Blinker's async implementation. See `Blinker async`_ .. tip:: Remember to add ``**extra_args`` to your signature so that if we add additional parameters in the future your code doesn't break. See the documentation for the signals provided by the Flask-Login and Flask-Principal extensions. In addition to those signals, Flask-Security sends the following signals. .. data:: user_authenticated Sent when a user successfully authenticates. In addition to the app (which is the sender), it is passed `user`, and `authn_via` arguments. The `authn_via` argument specifies how the user authenticated - it will be a list with possible values of ``password``, ``sms``, ``authenticator``, ``email``, ``confirm``, ``reset``, ``register``, ``webauthn``, ``oauth``. .. versionadded:: 3.4.0 .. data:: user_unauthenticated Sent when a user accesses a protected endpoint but isn't properly authenticated. It is sent as part of the `default_unauthn_handler` (:meth:`.Security.unauthn_handler`) It is passed the app (which is the sender). .. versionadded:: 5.4.0 .. data:: user_failed_authn Sent from :py:meth:`.UserMixin.track_failed_authn` when a user fails authentication. In addition to the app (which is the sender) it is passed: * `user` - the user that failed authentication * `endpoint` - the endpoint that was accessed * `auth_type` - `password`, `code`, `passkey` * `tfa` - True if this was a two factor authentication attempt .. versionadded:: 5.8.0 .. data:: user_registered Sent when a user registers on the site. In addition to the app (which is the sender), it is passed `user`, `confirm_token` (deprecated), `confirmation_token` and `form_data` arguments. `form_data` is a dictionary representation of registration form's content received with the registration request. .. data:: user_not_registered Sent when a user attempts to register, but is already registered. This is ONLY sent when :py:data:`SECURITY_RETURN_GENERIC_RESPONSES` is enabled. It is passed the following arguments: * `user` - The existing user model * `existing_email` - True if attempting to register an existing email * `existing_username`- True if attempting to register an existing username * `form_data` - the entire contents of the posted request form .. versionadded:: 5.0.0 .. data:: user_confirmed Sent when a user is confirmed. In addition to the app (which is the sender), it is passed a `user` argument. .. data:: confirm_instructions_sent Sent when a user requests confirmation instructions. In addition to the app (which is the sender), it is passed a `user` and `confirmation_token` arguments. .. data:: login_instructions_sent Sent when passwordless login is used and user logs in. In addition to the app (which is the sender), it is passed `user` and `login_token` arguments. .. data:: password_reset Sent when a user completes a password reset. In addition to the app (which is the sender), it is passed a `user` argument. .. data:: password_changed Sent when a user completes a password change. In addition to the app (which is the sender), it is passed a `user` argument. .. data:: reset_password_instructions_sent Sent when a user requests a password reset. In addition to the app (which is the sender), it is passed `user`, `token` (deprecated), and `reset_token` arguments. .. data:: change_email_instructions_sent Sent when a user requests to change their registered email address. In addition to the app (which is the sender) it is passed `user`, `token`, and `new_email`. .. versionadded:: 5.5.0 .. data:: change_email_confirmed Sent when a user has confirmed their new email address. In addition to the app (which is the sender) it is passed `user`, `old_email`. .. versionadded:: 5.5.0 .. data:: refresh_tracker_created Sent when a user has authenticated, has requested an auth_token, and the :py:data:`SECURITY_REFRESH_TOKEN` feature is enabled. In addition to the app (which is the sender), it is passed the `user` and the `refresh_tracker` objects. .. versionadded:: 5.9.0 .. data:: refresh_tracker_revoked Sent when a refresh tracker is revoked as part of the ``/refresh`` view when the provided refresh token doesn't match the current generation #. In addition to the app (which is the sender), it is passed the `user` and the `refresh_tracker` objects, and the `refresh_error` tuple. .. versionadded:: 5.9.0 .. data:: tf_code_confirmed Sent when a user performs two-factor authentication login on the site. In addition to the app (which is the sender), it is passed `user` and `method` arguments. .. versionadded:: 3.3.0 .. data:: tf_profile_changed Sent when two-factor is used and user logs in. In addition to the app (which is the sender), it is passed `user` and `method` arguments. .. versionadded:: 3.3.0 .. data:: tf_disabled Sent when two-factor is disabled. In addition to the app (which is the sender), it is passed `user` argument. .. versionadded:: 3.3.0 .. data:: tf_security_token_sent Sent when a two factor security/access code is sent. In addition to the app (which is the sender), it is passed `user`, `method`, `login_token` and `token` (deprecated) arguments. .. versionadded:: 3.3.0 .. data:: username_changed Sent when a username is successfully changed. In addition to the app (which is the sender), it is passed the `user` and `old_username` arguments. .. data:: username_recovery_email_sent Sent when a username is successfully recovered and sent over email. In addition to the app (which is the sender), it is passed the `user` argument. .. versionadded:: 5.6.0 .. data:: us_security_token_sent Sent when a unified sign in access code is sent. In addition to the app (which is the sender), it is passed `user`, `method`, `token` (deprecated), `login_token`, `phone_number`, and `send_magic_link` arguments. .. versionadded:: 3.4.0 .. data:: us_profile_changed Sent when user completes changing their unified sign in profile. In addition to the app (which is the sender), it is passed `user`, `methods`, and `delete` arguments. `delete` will be set to ``True`` if the user removed a sign in option. .. versionadded:: 3.4.0 .. versionchanged:: 5.0.0 Added delete argument and changed `method` to `methods` which is now a list. .. data:: wan_registered Sent when a WebAuthn credential was successfully created. In addition to the app (which is the sender), it is passed `user` and `name` arguments. .. versionadded:: 5.0.0 .. data:: wan_deleted Sent when a WebAuthn credential was deleted. In addition to the app (which is the sender), it is passed `user` and `name` arguments. .. versionadded:: 5.0.0 .. _Flask async: https://flask.palletsprojects.com/en/3.0.x/async-await/#using-async-and-await .. _Flask documentation on signals: https://flask.palletsprojects.com/en/2.3.x/signals/ .. _Blinker async: https://blinker.readthedocs.io/en/stable/#async-receivers pallets-eco-flask-security-72bf929/docs/authors.rst000066400000000000000000000000301525721743500224060ustar00rootroot00000000000000.. include:: ../AUTHORS pallets-eco-flask-security-72bf929/docs/changelog.rst000066400000000000000000000000341525721743500226540ustar00rootroot00000000000000.. include:: ../CHANGES.rst pallets-eco-flask-security-72bf929/docs/conf.py000066400000000000000000000153731525721743500215060ustar00rootroot00000000000000# # Flask-Security documentation build configuration file, created by # sphinx-quickstart on Mon Mar 12 15:35:21 2012. # # This file is execfile()d with the current directory set to its containing # dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys from pallets_sphinx_themes import ProjectLink from pallets_sphinx_themes import get_version # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath("..")) # -- General configuration ----------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ "pallets_sphinx_themes", "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.autosectionlabel", "sphinx_issues", ] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix of source filenames. source_suffix = ".rst" # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = "index" # General information about the project. project = "Flask-Security" copyright = "2012-2026" author = "Matt Wright & Chris Wagner" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # release, version = get_version("Flask-Security") # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. # pygments_style = "pocoo" # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] nitpicky = True nitpick_ignore = [ ("py:class", "mongoengine.connection"), ("py:class", "ResponseValue"), ("py:class", "AuthenticatorSelectionCriteria"), ("py:class", "UserVerificationRequirement"), ("py:class", "OAuth"), ("py:class", "OAuthError"), ("py:class", "authlib.integrations.flask_client.OAuth"), ("py:class", "TotpMatch"), ("py:class", "t.Type"), ("py:class", "t.Callable"), ("py:class", "t.Any"), ("py:class", "timedelta"), ("py:class", "wtforms.fields.core.Field"), ] autodoc_typehints = "description" # autodoc_mock_imports = ["flask_sqlalchemy"] autodoc_type_aliases = { "CbType": "oauth_provider.CbType", } autosectionlabel_prefix_document = True autosectionlabel_maxdepth = 2 intersphinx_mapping = { "python": ("https://docs.python.org/3/", None), "werkzeug": ("https://werkzeug.palletsprojects.com/", None), "flask": ("https://flask.palletsprojects.com/", None), "itsdangerous": ("https://itsdangerous.palletsprojects.com/", None), "sqlalchemy": ("https://docs.sqlalchemy.org/", None), "wtforms": ("https://wtforms.readthedocs.io/", None), "flask_wtforms": ("https://flask-wtf.readthedocs.io", None), "flask_sqlalchemy": ("https://flask-sqlalchemy.palletsprojects.com/", None), "flask_sqlalchemy_lite": ( "https://flask-sqlalchemy-lite.readthedocs.io/en/latest/", None, ), "flask_login": ("https://flask-login.readthedocs.io/en/latest/", None), "passlib": ("https://passlib.readthedocs.io/en/stable", None), "authlib": ("https://docs.authlib.org/en/latest/", None), } # -- Options for HTML output --------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = "flask" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = {"index_sidebar_logo": False} html_context = { "project_links": [ ProjectLink("PyPI releases", "https://pypi.org/project/Flask-Security/"), ProjectLink("Source Code", "https://github.com/pallets-eco/flask-security/"), ProjectLink( "Issue Tracker", "https://github.com/pallets-eco/flask-security/issues/", ), ProjectLink( "Changes", "https://flask-security.readthedocs.io/en/stable/changelog.html", ), ] } # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = "Flask-Security Documentation ({}).format(version)" html_logo = "_static/logo-owl-105.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static", "openapi.yaml"] # Custom sidebar templates, maps document names to template names. html_sidebars = { "index": ["project.html", "localtoc.html", "searchbox.html"], "**": ["relations.html", "searchbox.html", "localtoc.html"], } singlehtml_sidebars = {"index": ["project.html", "localtoc.html"]} # If true, links to the reST sources are added to the pages. html_show_sourcelink = False # Output file base name for HTML help builder. htmlhelp_basename = "Flask-Securitydoc" # -- Options for sphinx-issues --------------------------------------------- # Github repo issues_github_path = "pallets-eco/flask-security" pallets-eco-flask-security-72bf929/docs/configuration.rst000066400000000000000000002475361525721743500236200ustar00rootroot00000000000000Configuration ============= .. warning:: Be sure to set all configuration (in app.config["xxx"]) *PRIOR* to instantiating the Security class or calling security.init_app(). The following configuration values are used by Flask-Security: Core -------------- These configuration keys are used globally across all features. .. py:data:: SECRET_KEY This is actually part of Flask - but is used by Flask-Security to sign all tokens. It is critical this is set to a strong value. For python3 consider using: ``secrets.token_urlsafe()`` .. py:data:: SECRET_KEY_FALLBACKS This is part of Flask (>=3.1) but can be used by Flask-Security to unsign tokens. See Flask documentation https://flask.palletsprojects.com/en/stable/config/#SECRET_KEY_FALLBACKS .. versionadded:: 5.6.0 .. py:data:: SECURITY_BLUEPRINT_NAME Specifies the name for the Flask-Security blueprint. Default: ``"security"``. .. py:data:: SECURITY_URL_PREFIX Specifies the URL prefix for the Flask-Security blueprint. Default: ``None``. .. py:data:: SECURITY_STATIC_FOLDER Specifies the folder name for static files (webauthn). Default: ``"static"``. .. versionadded:: 5.1.0 .. py:data:: SECURITY_STATIC_FOLDER_URL Specifies the URL for static files used by Flask-Security (webauthn). See Flask documentation https://flask.palletsprojects.com/en/latest/blueprints/#static-files Default: ``"/fs-static"``. .. versionadded:: 5.1.0 .. py:data:: SECURITY_SUBDOMAIN Specifies the subdomain for the Flask-Security blueprint. If your authenticated content is on a different subdomain, also enable :py:data:`SECURITY_REDIRECT_ALLOW_SUBDOMAINS`. Default: ``None``. .. py:data:: SECURITY_FLASH_MESSAGES Specifies whether or not to flash messages for actions certain endpoint perform. Normally Flash-Security views will flash informational or error messages only when the operation results in a redirect. Default: ``True``. .. py:data:: SECURITY_INPUT_NORMALIZE_FORM Forms that take unstructured strings (e.g. username, webauthn name) are sanitized (using nh3), validated for specific unicode categories and optionally normalized using Python unicodedata.normalize() method Default: ``"NFKD"`` .. versionadded:: 5.9.0 .. py:data:: SECURITY_I18N_DOMAIN Specifies the name for domain used for translations. Default: ``"flask_security"`` .. py:data:: SECURITY_I18N_DIRNAME Specifies the directory containing the ``MO`` files used for translations. When using flask-babel this can also be a list of directory names - this enables application to override a subset of messages if desired. The default ``builtin`` uses translations shipped with Flask-Security. Default: ``"builtin"``. .. versionchanged:: 5.2.0 "builtin" is a special name which will be interpreted as the ``translations`` directory within the installation of Flask-Security. .. py:data:: SECURITY_TOKEN_AUTHENTICATION_KEY Specifies the query string parameter to read when using token authentication. Default: ``"auth_token"``. .. py:data:: SECURITY_TOKEN_AUTHENTICATION_HEADER Specifies the HTTP header to read when using token authentication. Default: ``"Authentication-Token"``. .. py:data:: SECURITY_TOKEN_MAX_AGE Specifies token expiration as a timedelta. Setting to 0 will mean the token never expires. Default: ``timedelta(minutes=15)`` .. versionchanged:: 5.9.0 Change from int to a timedelta. If an int is provided, it will be changed to a timedelta as part of Flask-Security initialization. .. py:data:: SECURITY_TOKEN_EXPIRE_TIMESTAMP A callable that returns a unix timestamp in the future when this specific authentication token should expire. Returning 0 means no expiration. It is passed the currently authenticated User so any fields can be used to customize an expiration time. Of course it is called in a request context so any information about the current request can also be used. If BOTH this and :data:`SECURITY_TOKEN_MAX_AGE` are set - the shorter is used. .. note:: These 2 expiry options work differently - with this one, the actual expire timestamp is in the auth_token. The signed token (using itsdangerous) has the timestamp the token was generated. On validation, that is checked against ``SECURITY_TOKEN_MAX_AGE``. So for MAX_AGE, at the time of validation, the token hasn't yet been associated with a User. Default: ``lambda user: 0`` .. py:data:: SECURITY_CACHE_CONTROL Specifies the Cache-Control header attributes to be sent with all responses from Flask-Security endpoints. Each entry in the dict is a (attribute, value) pair, where allowed attributes are defined in the Flask documentation https://flask.palletsprojects.com/en/stable/api/#flask.Response.cache_control. .. note:: Flask-Security does not pretend to handle the Cache-Control header for your application. This default setting is to ensure that out of the box, Flask-Security responses which might contain sensitive information aren't cached. Please see https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control#cache_directives. Responses from your own application endpoints (including endpoints protected with a Flask-Security decorator, such as token-authenticated APIs) are not modified - set the Cache-Control header there yourself if needed. .. note:: Flask itself adds the 'Vary: Cookie' header to all responses that access the session (cookie). This of course includes all non-anonymous Flask-Security endpoints as well as any application endpoints that are decorated with a Flask-Security decorator. This alone is likely enough to ensure no personal information is leaked. Default: ``{"private": True, "no-store": True}``. .. versionadded:: 5.8.0 .. py:data:: SECURITY_EMAIL_VALIDATOR_ARGS Email address are validated and normalized via the ``mail_util_cls`` which defaults to :class:`.MailUtil`. That uses the `email_validator`_ package whose methods have configurable options - these can be set here and will be passed in. For example setting this to: ``{"check_deliverability": False}`` is useful when unit testing if the emails are fake. ``mail_util_cls`` has 2 methods - ``normalize`` and ``validate``. Both ensure the passed value is a valid email address, and returns a normalized version. ``validate`` additionally, by default, verifies that the email address can likely actually receive an email. Default: ``None``, meaning use the defaults from email_validator package. .. versionadded:: 4.0.0 .. _email_validator: https://pypi.org/project/email-validator/ .. py:data:: SECURITY_DEFAULT_HTTP_AUTH_REALM Specifies the default authentication realm when using basic HTTP auth. Default: ``Login Required`` .. py:data:: SECURITY_REDIRECT_BEHAVIOR Passwordless login, confirmation, reset password, unified signin, change_email, and Social Login have GET endpoints that validate the passed token and redirect to an action form. For Single-Page-Applications style UIs which need to control their own internal URL routing these redirects need to not contain forms, but contain relevant information as query parameters. Setting this to ``"spa"`` will enable that behavior. When this is enabled, the following must also be defined: - :py:data:`SECURITY_POST_OAUTH_LOGIN_VIEW` (if :py:data:`SECURITY_OAUTH_ENABLE` is True) - :py:data:`SECURITY_POST_OAUTH_VERIFY_VIEW` (if :py:data:`SECURITY_OAUTH_ENABLE` is True) - :py:data:`SECURITY_VERIFY_ERROR_VIEW` (if :py:data:`SECURITY_OAUTH_ENABLE` is True) - :py:data:`SECURITY_LOGIN_ERROR_VIEW` - :py:data:`SECURITY_CONFIRM_ERROR_VIEW` (if :py:data:`SECURITY_CONFIRMABLE` is True) - :py:data:`SECURITY_POST_CHANGE_EMAIL_VIEW` (if :py:data:`SECURITY_CHANGE_EMAIL` is True) - :py:data:`SECURITY_CHANGE_EMAIL_ERROR_VIEW` (if :py:data:`SECURITY_CHANGE_EMAIL` is True) - :py:data:`SECURITY_POST_CONFIRM_VIEW` (if :py:data:`SECURITY_CONFIRMABLE` is True) - :py:data:`SECURITY_RESET_ERROR_VIEW` (if :py:data:`SECURITY_RECOVERABLE` is True) - :py:data:`SECURITY_RESET_VIEW` (if :py:data:`SECURITY_RECOVERABLE` is True) Default: ``None`` which is existing html-style form redirects. .. versionadded:: 3.3.0 .. py:data:: SECURITY_REDIRECT_HOST Mostly for development purposes, the UI is often developed separately and is running on a different port than the Flask application. In order to test redirects, the `netloc` of the redirect URL needs to be rewritten. Setting this to e.g. `localhost:8080` does that. .. tip:: Be aware that when this is set, any of the `*_VIEW` configuration variables that are set to URLs and not endpoints, will be redirected to this host. Default: ``None``. .. versionadded:: 3.3.0 .. py:data:: SECURITY_REDIRECT_ALLOW_SUBDOMAINS If ``True`` then subdomains (and the root domain) of the top-level host set by Flask's ``SERVER_NAME`` configuration will be allowed as post-view redirect targets. This is beneficial if you wish to place your authentication on one subdomain and authenticated content on another, for example ``auth.domain.tld`` and ``app.domain.tld``. Default: ``False``. .. versionadded:: 4.0.0 .. py:data:: SECURITY_REDIRECT_BASE_DOMAIN Set the base domain for checking allowable redirects. The intent here is to allow an application to be served on e.g. "flaskapp.my.org" and redirect to "myservice.my.org" (which maybe isn't a Flask app). Flask's SERVER_NAME can't be used to verify redirects in this case. Note that in most cases the application will want to set Flask's SESSION_COOKIE_DOMAIN to be this base domain - otherwise authorization information won't be sent. Default: ``None`` .. versionadded:: 5.5.0 .. py:data:: SECURITY_REDIRECT_ALLOWED_SUBDOMAINS A list of subdomains. Each will be prepended to ``SECURITY_REDIRECT_BASE_DOMAIN`` and checked against the requested redirect. If the list includes '.', the base domain itself will be checked against the redirect. Default: ``[]`` .. versionadded:: 5.5.0 .. versionchanged:: 5.9.0 Add support for using '.' to signify checking against the base domain value itself. .. note:: The above 4 config options apply BOTH to the handling of ``next`` parameter as well as all the ``XXX_VIEW`` URL configuration options for those views that perform a redirect after processing. .. py:data:: SECURITY_CSRF_PROTECT_MECHANISMS Authentication mechanisms that require CSRF protection. These are the same mechanisms as are permitted in the ``@auth_required`` decorator. Default: ``("basic", "session", "token")``. .. py:data:: SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS If ``True`` then CSRF will not be required for endpoints that don't require authentication (e.g. login, logout, register, forgot_password). Default: ``False``. .. py:data:: SECURITY_CSRF_COOKIE_NAME The name for the CSRF cookie. This usually should be dictated by your client-side code - more information can be found at :ref:`csrf_topic` Default: ``None`` - meaning no cookie will be sent. .. py:data:: SECURITY_CSRF_COOKIE A dict that defines the parameters required to set a CSRF cookie. The complete set of parameters is described in Flask's `set_cookie`_ documentation. Default: ``{"samesite": "Strict", "httponly": False, "secure": True}`` .. versionchanged:: 4.1.0 The 'key' attribute was deprecated in favor of a separate configuration variable :data:`SECURITY_CSRF_COOKIE_NAME`. .. versionchanged:: 5.9.0 ``secure`` default changed to ``True`` .. py:data:: SECURITY_CSRF_HEADER The HTTP Header name that will contain the CSRF token. ``X-XSRF-Token`` is used by packages such as `axios`_. Default: ``"X-XSRF-Token"``. .. py:data:: SECURITY_CSRF_COOKIE_REFRESH_EACH_REQUEST By default, csrf_tokens have an expiration (controlled by the configuration variable ``WTF_CSRF_TIME_LIMIT``. This can cause CSRF failures if say an application is left idle for a long time. You can set that time limit to ``None`` or have the CSRF cookie sent on every request (which will give it a new expiration time). Default: ``False``. .. py:data:: SECURITY_SCRIPT_NONCE_KEY If set, adds a nonce attribute to all `` {% endblock head_scripts %} {% block content %} {% include "security/_messages.html" %}

{{ _fsdomain("Setup a New Passkey") }}

{% if not credential_options %} {# Initial form to get CreateOptions #}
{{ _fsdomain("Start by providing a unique name for your passkey:") }}
{{ wan_register_form.hidden_tag() }} {{ render_field_with_errors(wan_register_form.name) }} {# Default is just second factor #} {% if config["SECURITY_WAN_ALLOW_AS_FIRST_FACTOR"] %}
{% for subfield in wan_register_form.usage %}{{ render_field_with_errors(subfield) }}{% endfor %}
{% endif %} {{ render_field(wan_register_form.submit) }}
{% else %}
{{ wan_register_response_form.hidden_tag() }}
{% endif %} {% if registered_credentials %}

{{ _fsdomain("Currently registered passkeys:") }}

{% set listing = _fsdomain('Nickname: "%s" Usage: "%s" Transports: "%s" Discoverable: "%s" Device Type: "%s" Backed up? "%s" Last used on: %s') %}
    {% for cred in registered_credentials %}
  • {{ listing|format(cred.name, cred.usage, cred.transports|join(", "), cred.discoverable, cred.device_type, cred.backup_state, cred.lastuse) }}
  • {% endfor %}
{% endif %} {% if wan_delete_form %}

{{ _fsdomain("Delete an Existing Passkey") }}

{# explicitly render csrf_token so we can change the ID so we don't get duplicates #} {{ render_csrf(wan_delete_form, "delete") }} {{ render_field_with_errors(wan_delete_form.name) }} {{ render_field(wan_delete_form.submit) }}
{% endif %} {% if security.support_mfa and security.multi_factor_recovery_codes %}

{{ _fsdomain("Recovery Codes") }}

{{ _fsdomain("This application supports setting up recovery codes.") }} {{ _fsdomain("You can set them up here.") }}
{% endif %} {% include "security/_menu.html" %} {% endblock content %} pallets-eco-flask-security-72bf929/flask_security/templates/security/wan_signin.html000066400000000000000000000053051525721743500311670ustar00rootroot00000000000000{# This template receives the following pieces of context in addition to the form: #} {% extends "security/base.html" %} {% set title = title|default(_fsdomain("Sign In With A Passkey")) %} {% from "security/_macros.html" import render_field_with_errors, render_field, render_field_errors, render_form_errors, prop_next, script_nonce %} {% block head_scripts %} {{ super() }} {% endblock head_scripts %} {% block content %} {% include "security/_messages.html" %} {% if not is_secondary %}

{{ _fsdomain("Sign In With a Passkey") }}

{% else %}

{{ _fsdomain("Use a Passkey as a Second Factor") }}

{% endif %} {% if not credential_options %}
{{ wan_signin_form.hidden_tag() }} {{ render_form_errors(wan_signin_form) }} {% if not is_secondary %} {{ render_field_with_errors(wan_signin_form.identity) }} {{ render_field_with_errors(wan_signin_form.remember) }} {% endif %} {{ render_field_errors(wan_signin_form.credential) }} {{ render_field_errors(wan_signin_form.csrf_token) }} {{ render_field(wan_signin_form.submit) }}
{% else %}
{{ wan_signin_response_form.hidden_tag() }} {{ render_field_errors(wan_signin_form.remember) }} {# the following is important even though it is hidden - some browsers require an input focus field (such as Safari) #} {{ render_field(wan_signin_response_form.credential) }}
{% endif %} {% endblock content %} pallets-eco-flask-security-72bf929/flask_security/templates/security/wan_verify.html000066400000000000000000000040721525721743500312040ustar00rootroot00000000000000{# This template receives the following pieces of context in addition to the form: wan_verify_form - wan_signin_response_form - skip_login_menu - True Any other context provided by the "wan_verify" context processer. #} {% extends "security/base.html" %} {% set title = title|default(_fsdomain("Reauthenticate")) %} {% from "security/_macros.html" import render_field_with_errors, render_field, prop_next, script_nonce %} {% block head_scripts %} {{ super() }} {% endblock head_scripts %} {% block content %} {% include "security/_messages.html" %}

{{ _fsdomain("Reauthenticate Using a Passkey") }}

{% if not credential_options %}
{{ wan_verify_form.hidden_tag() }} {{ render_field(wan_verify_form.submit) }}
{% else %}
{{ wan_signin_response_form.hidden_tag() }}
{% endif %} {% endblock content %} pallets-eco-flask-security-72bf929/flask_security/tf_plugin.py000066400000000000000000000306051525721743500246420ustar00rootroot00000000000000""" flask_security.tf_plugin ~~~~~~~~~~~~~~~~~~~~~~~~ Flask-Security Two-Factor Plugin Module :copyright: (c) 2022-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. TODO: - add localized callback for select choices. """ from __future__ import annotations import typing as t from flask import request, redirect, session from .decorators import unauth_csrf from .forms import ( _build_form_from_request, get_form_field_xlate, Form, RadioField, SubmitField, ) from .proxies import _datastore, _security from .utils import ( _, base_render_json, check_and_get_token_status, _config_value as cv, do_flash, get_message, get_url, login_user, _propagate_next, simple_render_json, url_for_security, ) if t.TYPE_CHECKING: # pragma: no cover import flask from flask.typing import ResponseValue from flask import Response from flask_security import Security, UserMixin class TwoFactorSelectForm(Form): which: RadioField = RadioField( get_form_field_xlate(_("Available Second Factor Methods:")) ) submit: SubmitField = SubmitField(get_form_field_xlate(_("Select"))) def __init__(self, *args: t.Any, **kwargs: t.Any): super().__init__(*args, **kwargs) @unauth_csrf() def tf_select() -> ResponseValue: # Ask user which MFA method they want to use. # This is used when a user has setup more than one type of 2FA. form = t.cast( TwoFactorSelectForm, _build_form_from_request("two_factor_select_form") ) # This endpoint is unauthenticated - make sure we're in a valid state if not all(k in session for k in ["tf_user_id", "tf_select"]): # illegal call on this endpoint tf_clean_session() return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW")) user = _datastore.find_user(fs_uniquifier=session["tf_user_id"]) if not user: # pragma no cover # hard to imagine - someone deletes the user while they are logging in. tf_clean_session() return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW")) setup_methods = _security.two_factor_plugins.get_setup_tf_methods(user) form.which.choices = setup_methods # type: ignore[assignment] if form.validate_on_submit(): response = None tf_impl = _security.two_factor_plugins.method_to_impl(user, form.which.data) if tf_impl: json_payload = {"tf_required": True} response = tf_impl.tf_login( user, json_payload, next_loc=_propagate_next(request.url, None) ) if not response: # pragma no cover # This really can't happen unless between the time the started logging in # and now, they deleted a second factor (which they would have to do # in another window). tf_clean_session() return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW")) return response if _security._want_json(request): payload = {"tf_select": True, "tf_setup_methods": [k for k, v in setup_methods]} return base_render_json(form, include_user=False, additional=payload) return _security.render_template( cv("TWO_FACTOR_SELECT_TEMPLATE"), two_factor_select_form=form, **_security._run_ctx_processor("tf_select"), ) class TfPluginBase: # pragma no cover def __init__(self, app: flask.Flask): pass def create_blueprint( self, app: flask.Flask, bp: flask.Blueprint, state: Security ) -> None: raise NotImplementedError def get_setup_methods(self, user: UserMixin) -> list[tuple[str, str]]: """Return a list of tuples representing currently configured methods. The tuple is (value, label) - suitable for use in a FlaskForm Select element. """ raise NotImplementedError def tf_login( self, user: UserMixin, json_payload: dict[str, t.Any], next_loc: str | None ) -> ResponseValue: """ Called from first/primary authenticated views if the user successfully authenticated, and required a second method of authentication. This method returns the necessary information for the user UI to continue. For forms, this is usually a redirect to a secondary sign in form. For JSON it is just a payload that describes what the user has to do next. """ raise NotImplementedError class TfPlugin: """ Two-Factor plugin support. Enables multiple independent two-factor implementations to be configured for a given app. See TfPluginBase for what a new implementation must provide. """ def __init__(self) -> None: self._tf_impls: dict[str, TfPluginBase] = {} def register_tf_impl( # N.B. all methods must be unique across all implementations. self, app: flask.Flask, name: str, impl: t.Type[TfPluginBase], ) -> None: self._tf_impls[name] = impl(app) def create_blueprint( self, app: flask.Flask, bp: flask.Blueprint, state: Security ) -> None: if state.support_mfa: for impl in self._tf_impls.values(): impl.create_blueprint(app, bp, state) # Add our route for selecting between multiple active two-factor # mechanisms. bp.route( cv("TWO_FACTOR_SELECT_URL", app), methods=["GET", "POST"], endpoint="tf_select", )(tf_select) def method_to_impl(self, user: UserMixin, method: str) -> TfPluginBase | None: # reverse map a method to the implementation. # N.B. again - requires that methods be unique across all implementations. # There is a small window that a previously setup method was removed. for impl in self._tf_impls.values(): setup_methods = impl.get_setup_methods(user) if method in [k for k, v in setup_methods]: return impl return None # pragma no cover def get_setup_tf_methods(self, user: UserMixin) -> list[tuple[str, str]]: """Return a list of tuples representing currently configured methods. The tuple is (value, translated(value)) - suitable for use in a FlaskForm Select element. """ methods = [] for impl in self._tf_impls.values(): methods.extend(impl.get_setup_methods(user)) return methods def tf_enter( self, user: UserMixin, remember_me: bool | None, primary_authn_via: str, next_loc: str | None, ) -> ResponseValue | None: """Check if two-factor is required and if so, start the process. Must be called in a request context. remember_me controls 2 cookies - the remember_me cookie and the tf_validity cookie. We use the session to hold the fact that the user requested 'remember' across the second factor. """ json_payload: dict[str, t.Any] if _security.support_mfa: tf_setup_methods = self.get_setup_tf_methods(user) tf_fresh = tf_verify_validity_token(user.fs_uniquifier) tf_required, tf_available_methods = user.check_tf_required( tf_setup_methods, tf_fresh ) if tf_required: tf_setup_methods_keys = [t1 for t1, t2 in tf_setup_methods] # Clean out any potential old session info - in case of previous # aborted 2FA attempt. tf_clean_session() json_payload = {"tf_required": True} if remember_me: session["tf_remember_login"] = remember_me session["tf_user_id"] = user.fs_uniquifier # A backwards compat hack - the original twofactor could be setup # as part of initial login. if len(tf_setup_methods) == 0: # only initial two-factor implementation supports this return self._tf_impls["code"].tf_login(user, json_payload, next_loc) elif len(tf_setup_methods) == 1: # method_to_impl can't return None here since we just # got the methods up above. impl = t.cast( TfPluginBase, self.method_to_impl(user, tf_setup_methods_keys[0]), ) return impl.tf_login(user, json_payload, next_loc) else: session["tf_select"] = True if not _security._want_json(request): values = dict(next=next_loc) if next_loc else dict() return redirect(url_for_security("tf_select", **values)) # Let's force app to go through tf-select just in case we want # to do further validation... However, provide the choices # so they can just do a POST json_payload.update( { "tf_select": True, "tf_setup_methods": tf_setup_methods_keys, } ) return simple_render_json(json_payload) return None def tf_complete(self, user: UserMixin, dologin: bool) -> str | None: remember = session.pop("tf_remember_login", None) if dologin: login_user(user, remember=remember) tf_clean_session() token = None # return a token to avoid future two-factor prompts (for a period of time) if not cv("TWO_FACTOR_ALWAYS_VALIDATE") and remember: token = generate_tf_validity_token(user.fs_uniquifier) return token def generate_tf_validity_token(fs_uniquifier): """Generates a unique token for the specified user. :param fs_uniquifier: The fs_uniquifier of a user to whom the token belongs to """ return _security.tf_validity_serializer.dumps(fs_uniquifier) def tf_validity_token_status(token): """Returns the expired status, invalid status, and user of a Two-Factor Validity token. For example:: expired, invalid, user = tf_validity_token_status('...') :param token: The Two-Factor Validity token """ return check_and_get_token_status( token, "tf_validity", cv("TWO_FACTOR_LOGIN_VALIDITY") ) def tf_verify_validity_token(fs_uniquifier: str) -> bool: """Returns the status of the Two-Factor Validity token based on the current request. :param fs_uniquifier: The ``fs_uniquifier`` of the submitting user. """ token = request.cookies.get(cv("TWO_FACTOR_VALIDITY_COOKIE_NAME"), default=None) if token is None: return False expired, invalid, uniquifier = tf_validity_token_status(token) if expired or invalid or (fs_uniquifier != uniquifier): return False return True def tf_set_validity_token_cookie(response: Response, token: str) -> Response: """Sets the Two-Factor validity cookie""" cookie_kwargs = cv("TWO_FACTOR_VALIDITY_COOKIE") max_age = int(cv("TWO_FACTOR_LOGIN_VALIDITY").total_seconds()) response.set_cookie( cv("TWO_FACTOR_VALIDITY_COOKIE_NAME"), value=token, max_age=max_age, **cookie_kwargs, ) # This is likely overkill since so far we only return this on a POST which is # unlikely to be cached. response.vary.add("Cookie") return response def tf_check_state(allowed_states: list[str]) -> UserMixin | None: if ( not all(k in session for k in ["tf_user_id", "tf_state"]) or session["tf_state"] not in allowed_states ): tf_clean_session() return None user = _datastore.find_user(fs_uniquifier=session["tf_user_id"]) if not user: tf_clean_session() return user def tf_illegal_state(form, redirect_to): m, c = get_message("TWO_FACTOR_PERMISSION_DENIED") if not _security._want_json(request): do_flash(m, c) return redirect(get_url(redirect_to)) else: form.form_errors.append(m) return base_render_json(form, include_user=False) def tf_clean_session(): """ Clean out ALL stuff stored in session (e.g. on logout or restart of a session) """ if cv("TWO_FACTOR"): for k in [ "tf_state", "tf_user_id", "tf_primary_method", "tf_remember_login", "tf_totp_secret", "tf_select", ]: session.pop(k, None) pallets-eco-flask-security-72bf929/flask_security/tokens.py000066400000000000000000000247151525721743500241630ustar00rootroot00000000000000""" flask_security.tokens ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Flask-Security Tokens module :copyright: (c) 2026-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. This module implements refresh and authentication tokens. - each time a refresh token is used, a new one on the same 'family' is created (by incrementing the 'gen' field) TODO - add ability for app to add/verify additional stuff in refresh_token - add endpoint to user can revoke refresh token? - implment rotation overlap period?: https://auth0.com/docs/secure/tokens/refresh-tokens/configure-refresh-token-rotation """ from __future__ import annotations from copy import copy from enum import Enum, auto import typing as t from functools import partial from flask import abort, request, after_this_request, current_app, Response from itsdangerous import BadSignature from wtforms import StringField from .decorators import unauth_csrf from .forms import ( Form, RequiredLocalize, SubmitField, _get_form_field_label, get_form_field_xlate, _build_form_from_request, IsString, ) from .proxies import _security, _datastore from .signals import refresh_tracker_revoked, refresh_tracker_created from .utils import ( _config_value as cv, _, _view_commit, get_message, base_render_json, ) if t.TYPE_CHECKING: # pragma: no cover from flask.typing import ResponseValue from .core import UserMixin, RefreshTrackerMixin class RefreshTokenErrors(Enum): INVALID = auto() EXPIRED = auto() NOT_FOUND = auto() GEN_MISMATCH = auto() REVOKED = auto() def response_tokens(user: UserMixin, payload: dict[str, t.Any]) -> None: """Views that authenticate and return JSON can also provide an auth token In addition, with the new refresh token feature, they should return a refresh token """ try: token = user.get_auth_token() except ValueError: # application has fs_token_uniquifier attribute but it # hasn't been initialized. Since we are in a request context # we can do that here. _datastore.set_token_uniquifier(user) after_this_request(_view_commit) token = user.get_auth_token() payload["user"]["authentication_token"] = token if _security.refresh_token: after_this_request(_view_commit) refresh_tracker, refresh_token = new_refresh_tracker(user, name="default") if cv("REFRESH_TOKEN_COOKIE_NAME"): after_this_request(partial(set_refresh_token_cookie, token=refresh_token)) else: payload["user"]["refresh_token"] = refresh_token def new_refresh_tracker(user: UserMixin, name: str) -> tuple[RefreshTrackerMixin, str]: """This creates a new refresh tracker and uses that to create a new refresh token Caller must commit to the datastore. """ # Take this opportunity to cleanup old refresh trackers _datastore.cleanup_refresh_trackers( user, expired=cv("REFRESH_TOKEN_CLEANUP_EXPIRED"), revoked=cv("REFRESH_TOKEN_CLEANUP_REVOKED"), ) expires_at = _security.datetime_factory() + cv("REFRESH_TOKEN_MAX_AGE") refresh_tracker = _datastore.create_refresh_tracker( user, name=name, expires_at=expires_at ) tdata: dict[str, t.Any] = { "ver": str(1), "uid": getattr(user, _datastore.get_token_uniquifier_name()), "expires_at": refresh_tracker.expires_at.timestamp(), "last_used_at": refresh_tracker.last_used_at.timestamp(), "name": name, "family": refresh_tracker.refresh_family, "gen": refresh_tracker.gen, } # Let application add things # user.augment_refresh_token(tdata) refresh_tracker_created.send( current_app._get_current_object(), # type: ignore _async_wrapper=current_app.ensure_sync, user=user, refresh_tracker=refresh_tracker, ) return refresh_tracker, _security.refresh_token_serializer.dumps(tdata) def get_refresh_token(refresh_tracker: RefreshTrackerMixin, user: UserMixin) -> str: """This creates a new refresh token from an existing refresh tracker""" tdata: dict[str, t.Any] = { "ver": str(1), "uid": getattr(user, _datastore.get_token_uniquifier_name()), "expires_at": refresh_tracker.expires_at.timestamp(), "last_used_at": refresh_tracker.last_used_at.timestamp(), "name": refresh_tracker.name, "family": refresh_tracker.refresh_family, "gen": refresh_tracker.gen, } # Let application add things # self.augment_refresh_token(tdata) return _security.refresh_token_serializer.dumps(tdata) def verify_refresh_token( token: str, ) -> tuple[RefreshTrackerMixin | None, RefreshTokenErrors | None, str | None]: """Verifies the refresh token and returns the refresh tracker Note that we always return the UID if the token is properly signed. This lets the calling code possibly find the user and be able to inform the application via signals""" try: tdata = _security.refresh_token_serializer.loads(token) except (BadSignature, TypeError, ValueError): return None, RefreshTokenErrors.INVALID, None if not (refresh_tracker := _datastore.find_refresh_tracker(tdata["family"])): return None, RefreshTokenErrors.NOT_FOUND, None if refresh_tracker.gen != tdata["gen"]: # this is considered an attack (or bad code) return refresh_tracker, RefreshTokenErrors.GEN_MISMATCH, tdata["uid"] if refresh_tracker.expires_at < _security.datetime_factory(): return refresh_tracker, RefreshTokenErrors.EXPIRED, tdata["uid"] if (_security.datetime_factory() - refresh_tracker.last_used_at) > cv( "REFRESH_TOKEN_MAX_IDLE" ): return refresh_tracker, RefreshTokenErrors.EXPIRED, tdata["uid"] if refresh_tracker.revoked_at and ( refresh_tracker.revoked_at < _security.datetime_factory() ): return refresh_tracker, RefreshTokenErrors.REVOKED, tdata["uid"] return refresh_tracker, None, tdata["uid"] def _revoke_refresh_tracker( refresh_tracker: RefreshTrackerMixin, refresh_errors: RefreshTokenErrors | None, user: UserMixin | None, ) -> None: """Helper function for views""" after_this_request(_view_commit) _datastore.revoke_refresh_tracker(refresh_tracker) refresh_tracker_revoked.send( current_app._get_current_object(), # type: ignore _async_wrapper=current_app.ensure_sync, user=user, refresh_tracker=refresh_tracker, refresh_errors=refresh_errors, ) class RefreshTokenForm(Form): """The refresh token form. Given a valid refresh token, this will return a new access token. """ refresh_token: StringField = StringField( get_form_field_xlate(_("Refresh Token")), validators=[IsString(), RequiredLocalize()], ) submit: SubmitField = SubmitField(label=_get_form_field_label("submit")) # returned to caller refresh_errors: RefreshTokenErrors | None = None refresh_tracker: RefreshTrackerMixin | None = None user: UserMixin | None = None def __init__(self, *args: t.Any, **kwargs: t.Any): super().__init__(*args, **kwargs) # If cookie name set - then use that - NOT anything in the form if request and cv("REFRESH_TOKEN") and cv("REFRESH_TOKEN_COOKIE_NAME"): token = request.cookies.get(cv("REFRESH_TOKEN_COOKIE_NAME"), default=None) self.refresh_token.data = token def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): # pragma: no cover return False assert self.refresh_token.data is not None # validator RequiredLocalize assert isinstance(self.refresh_token.errors, list) self.refresh_tracker, self.refresh_errors, uid = verify_refresh_token( self.refresh_token.data ) # Try to find user even if there are errors to aid logging if uid: self.user = _security.datastore.find_user( case_insensitive=False, **{_datastore.get_token_uniquifier_name(): uid} ) if self.refresh_errors: self.refresh_token.errors.append( get_message("REFRESH_TOKEN_INVALID", reason=self.refresh_errors.name)[0] ) return False if not self.user: self.refresh_token.errors.append(get_message("USER_DOES_NOT_EXIST")[0]) return False return True @unauth_csrf() def refresh() -> ResponseValue: if not request.is_json: abort(400) form: RefreshTokenForm = t.cast( RefreshTokenForm, _build_form_from_request("refresh_token_form") ) if form.validate_on_submit(): assert form.user assert form.refresh_tracker after_this_request(_view_commit) _datastore.exchange_refresh_tracker(form.refresh_tracker) payload = dict() payload["user"] = form.user.get_security_payload() payload["user"]["authentication_token"] = form.user.get_auth_token() refresh_token = get_refresh_token(form.refresh_tracker, form.user) if cv("REFRESH_TOKEN_COOKIE_NAME"): after_this_request(partial(set_refresh_token_cookie, token=refresh_token)) else: payload["user"]["refresh_token"] = refresh_token return _security._render_json(payload, 200, None, form.user) # Failed validation - if it was a GEN_MISMATCH this could be a hack and we should # invalidate/revoke the entire token family. if form.refresh_errors == RefreshTokenErrors.GEN_MISMATCH: assert form.refresh_tracker _revoke_refresh_tracker(form.refresh_tracker, form.refresh_errors, form.user) return base_render_json(form, include_user=False) def set_refresh_token_cookie(response: Response, token: str) -> Response: cookie_kwargs = copy(cv("REFRESH_TOKEN_COOKIE")) response.set_cookie(cv("REFRESH_TOKEN_COOKIE_NAME"), value=token, **cookie_kwargs) # This is likely overkill since so far we only return this on a POST which is # unlikely to be cached. response.vary.add("Cookie") return response def clear_refresh_token_cookie(response: Response) -> Response: cookie_kwargs = copy(cv("REFRESH_TOKEN_COOKIE")) # Alas delete_cookie only accepts some of the keywords set_cookie does allowed = ["path", "domain", "secure", "httponly", "samesite", "partitioned"] args = {k: cookie_kwargs.get(k) for k in allowed if k in cookie_kwargs} response.delete_cookie(cv("REFRESH_TOKEN_COOKIE_NAME"), **args) return response pallets-eco-flask-security-72bf929/flask_security/totp.py000066400000000000000000000163301525721743500236400ustar00rootroot00000000000000""" flask_security.totp ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Flask-Security TOTP (Timed-One-Time-Passwords) module :copyright: (c) 2019-2024 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from __future__ import annotations import base64 import io import typing as t from passlib.totp import TOTP, TokenError, TotpMatch from passlib.pwd import genword if t.TYPE_CHECKING: # pragma: no cover from flask_security import UserMixin class Totp: """Encapsulate usage of Passlib TOTP functionality. Flask-Security doesn't implement any replay-attack protection out of the box as suggested by: https://passlib.readthedocs.io/en/stable/narr/totp-tutorial.html#match-verify Subclass this and implement the get/set last_counter methods. Your subclass can be registered at Flask-Security creation/initialization time. .. versionadded:: 3.4.0 """ def __init__(self, secrets: dict[str, str] | dict[int, str], issuer: str): """Initialize a totp factory. secrets are used to encrypt the per-user totp_secret on disk. """ # This should be a dict with at least one entry if not isinstance(secrets, dict) or len(secrets) < 1: raise ValueError("secrets needs to be a dict with at least one entry") self._totp = TOTP.using(issuer=issuer, secrets=secrets) def generate_totp_password(self, totp_secret: str) -> str: """Get time-based one-time password on the basis of given secret and time :param totp_secret: the unique shared secret of the user """ tt = self._totp.from_source(totp_secret).generate() return tt.token # type: ignore[return-value] def generate_totp_secret(self) -> str: """Create new user-unique totp_secret. We return an encrypted json string so that when sent in a cookie or sent to DB - it is encrypted. """ return self._totp.new().to_json(encrypt=True) def verify_totp( self, token: str, totp_secret: str, user: UserMixin, window: int = 0 ) -> bool: """Verifies token for specific user. :param token: token to be check against user's secret :param totp_secret: the unique shared secret of the user :param user: User model :param window: optional. How far backward and forward in time to search for a match. Measured in seconds. :return: True if match """ # TODO - in old implementation using onetimepass window was described # as 'compensate for clock skew) and 'interval_length' would say how long # the token is good for. # In passlib - 'window' means how far back and forward to look and 'clock_skew' # is specifically for well, clock slew. try: tmatch = self._totp.verify( token, totp_secret, window=window, last_counter=self.get_last_counter(user), ) self.set_last_counter(user, tmatch) return True except TokenError: return False def get_totp_uri(self, username: str, totp_secret: str) -> str: """Generate provisioning url for use with the qrcode scanner built into the app :param username: username/email of the current user :param totp_secret: a unique shared secret of the user """ tp = self._totp.from_source(totp_secret) return tp.to_uri(username) def get_totp_pretty_key(self, totp_secret: str) -> str: """Generate pretty key for manual input :param totp_secret: a unique shared secret of the user .. versionadded:: 4.0.0 """ tp = self._totp.from_source(totp_secret) return tp.pretty_key() def get_totp_b32_key(self, totp_secret: str) -> str: """Generate b32 key for creating a otpauth url :param totp_secret: a unique shared secret of the user .. versionadded:: 5.8.0 """ tp = self._totp.from_source(totp_secret) return tp.base32_key def fetch_setup_values(self, totp: str, user: UserMixin) -> dict[str, str]: """Generate various values user needs to setup authenticator app. Returns dict with keys: 'key': totp key 'b32key': totp key in base32 'uri': provisioning url 'image': image as string (useful for ) 'username: qrcode best practice 'issuer': qrcode best practice .. versionadded:: 4.0.0 .. versionchanged:: 5.8.0 Added keys b32key and uri """ r = dict() # By convention, the URI should have the username that the user # logs in with. username = user.calc_username() or "Unknown" assert self._totp.issuer r["username"] = username r["key"] = self.get_totp_pretty_key(totp) r["b32key"] = self.get_totp_b32_key(totp) r["issuer"] = self._totp.issuer r["image"] = self.generate_qrcode(username, totp) r["uri"] = self.get_totp_uri(username, totp) return r def generate_qrcode(self, username: str, totp: str) -> str: """Generate QRcode Using username, totp, generate the actual QRcode image. This method can be overridden to fine-tune how the image is created - such as size, color etc. It must return a string suitable for use in an tag. .. versionadded:: 4.0.0 """ try: import qrcode import qrcode.image.svg image = qrcode.make( self.get_totp_uri(username, totp), image_factory=qrcode.image.svg.SvgImage, ) with io.BytesIO() as virtual_file: image.save(virtual_file) image_as_str = base64.b64encode(virtual_file.getvalue()).decode("ascii") return f"data:image/svg+xml;base64,{image_as_str}" except ImportError: # pragma: no cover # This should have been checked at app init. raise def generate_recovery_codes(self, number: int) -> list[str]: """Generate a set of secure passwords - used for 2FA recovery codes. # this is nice for english - but not for others return genphrase(entropy="fair", wordset="eff_short", sep="-", returns=number) .. versionadded:: 5.0.0 """ pwds = genword(length=12, charset="hex", returns=number) # make this a bit easier to type - 3 sets of 4 characters spwds = [] for pwd in pwds: spwds.append( "-".join([pwd[i : i + 4] for i in range(0, len(pwd), 4)]) # noqa: E203 ) return spwds def get_last_counter(self, user: UserMixin) -> int | None: """Implement this to fetch stored last_counter from cache. :param user: User model :return: last_counter as stored in set_last_counter() """ return None def set_last_counter(self, user: UserMixin, tmatch: TotpMatch) -> None: """Implement this to cache last_counter. :param user: User model :param tmatch: a TotpMatch as returned from totp.verify() """ pass pallets-eco-flask-security-72bf929/flask_security/translations/000077500000000000000000000000001525721743500250165ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/af_ZA/000077500000000000000000000000001525721743500257765ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/af_ZA/LC_MESSAGES/000077500000000000000000000000001525721743500275635ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/af_ZA/LC_MESSAGES/flask_security.po000066400000000000000000001260201525721743500331530ustar00rootroot00000000000000# Afrikaans (South Africa) translations for Flask-Security. # Copyright (C) 2021 Lonely Viking # This file is distributed under the same license as the Flask-Security # project. # Michael Bosch , 2021. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 4.0.0\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: Michael Bosch \n" "Language: af_ZA\n" "Language-Team: af_ZA \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:363 msgid "Login Required" msgstr "Intekening Benodig" #: flask_security/core.py:364 msgid "Welcome" msgstr "Welkom" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "Bevestig asseblief jou e-pos adres" #: flask_security/core.py:366 msgid "Login instructions" msgstr "Intekening instruksies" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "Jou wagwoord is teruggestel" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "Jou wagwoord is verander" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "Wagwoord terugstel instruksies" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:371 msgid "Your requested username" msgstr "" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:423 msgid "Verification Code" msgstr "Verifikasie Kode" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "Insette nie geskik vir die versoekte API nie" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "Jy het nie toestemming om hierdie hulpbron te bekyk nie." #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "Dankie. Jou e-pos adres is bevestig." #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "Jou e-pos adres is reeds bevestig." #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "" #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s is reeds geassosieer met 'n rekening." #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" "Identiteits kenmerk '%(attr)s' met waarde '%(value)s' is reeds " "geassosieer met 'n rekening." #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:531 msgid "Password does not match" msgstr "Wagwoord stem nie ooreen nie" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "Wagwoorde stem nie ooreen nie" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "Herleiding buite die domein is verbied" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "Instruksies om jou wagwoord terug te stel is gestuur na %(email)s." #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "Ongeldige wagwoord terugstellings token." #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "E-pos adres benodig bevestiging" #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Bevestigings instruksies is gestuur na %(email)s." #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Jy het nie ingeteken binne %(within)s nie. Nuwe instruksies om in te " "teken is gestuur na %(email)s." #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Instruksies om in te teken is gestuur na %(email)s." #: flask_security/core.py:565 msgid "Invalid login token." msgstr "Ongeldige intekenings token." #: flask_security/core.py:566 msgid "Account is disabled." msgstr "Rekening is gestremd." #: flask_security/core.py:567 msgid "Email not provided" msgstr "E-pos is nie voorsien nie" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "Ongeldige e-pos adres" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "Ongeldige kode" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "" #: flask_security/core.py:578 msgid "Password not provided" msgstr "Wagwoord is nie voorsien nie" #: flask_security/core.py:580 #, python-format msgid "Password must be at least %(length)s characters" msgstr "Wagwoord moet ten minste %(length)s karakters bevat" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "Wagwoord is te eenvoudig" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "Telefoon nommer is ongeldig, bv. afwesige lands kode" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "Gespesifieerde verbruiker bestaan nie" #: flask_security/core.py:591 msgid "Invalid password" msgstr "Ongeldige wagwoord" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "Voorsiende wagwoord of kode is ongeldig" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "Jy het met sukses in geteken" #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "Wagwoord vergeet?" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" "Jy het met sukses jou wagwoord teruggestel en jy het automaties in " "geteken." #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "Jou nuwe wagwoord moet verskil van jou vorige wagwoord." #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "Jy het met sukses jou wagwoord verander." #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "Teken asseblief in om toegang te kry tot hierdie blad." #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "Herverifieer asseblief om toegang te kry tot hierdie blad." #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "Herverifikasie suksesvol" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "Jy kan slegs hierdie eindpunt bereik wanneer jy nie in geteken is nie." #: flask_security/core.py:622 msgid "Code has been sent." msgstr "" #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "Mislukking met stuur van kode. Probeer asseblief later weer" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "Jy het met sukses jou twee-faktoor metode verander." #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "Jy het tans nie toestemming om hierdie blad te besoek nie" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "Gemerkde metode is nie geldig nie" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "Opstel moet voltooi wees binne %(within)s. Begin asseblief oor." #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "Versoekte metode is nie geldig nie" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "Jy moet 'n valiede identiteit spesifiseer om in te teken" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:667 msgid "Username not provided" msgstr "" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "Stel op deur midde van verifikasie toep (bv. google, lastpass, authy)" #: flask_security/forms.py:72 msgid "Change Method" msgstr "Verander Metode" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Verander Wagwoord" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "Verifikasie Kode" #: flask_security/forms.py:75 msgid "Delete" msgstr "" #: flask_security/forms.py:76 msgid "Email Address" msgstr "E-pos adres" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "Stel up deur midde van e-pos" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "Fout(e)" #: flask_security/forms.py:79 msgid "Identity" msgstr "Identiteit" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Teken In" #: flask_security/forms.py:81 msgid "New Password" msgstr "Nuwe Wagwoord" #: flask_security/forms.py:82 msgid "Passcode" msgstr "Wagkode" #: flask_security/forms.py:83 msgid "Password" msgstr "Wagwoord" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "Telefoon Nommer" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "Herstel Wagwoord" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Registreer" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "Onthou My" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "Stel Wagwoord Terug" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "Sleutel Weer Wagwoord In" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "Stuur Weer Bevestigings Instruksies" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "Stuur Intekenings Skakel" #: flask_security/forms.py:93 msgid "Send Code" msgstr "Stuur Kode" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Teken In" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "Stel op deur midde van SMS" #: flask_security/forms.py:96 msgid "Submit" msgstr "Stuur" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "Stuur Kode" #: flask_security/forms.py:98 msgid "Username" msgstr "" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "Bevestig Wagwoord" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:105 msgid "authenticator" msgstr "" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "" #: flask_security/forms.py:108 msgid "SMS" msgstr "" #: flask_security/forms.py:109 msgid "password" msgstr "" #: flask_security/forms.py:110 msgid "passkey" msgstr "" #: flask_security/forms.py:111 msgid "none" msgstr "" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "Beskikbare Metodes" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "Kode of Wagwoord" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "Via e-pos" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "Via SMS" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:241 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menu" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "Bevestig rekening" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Stuur wagwoord herstel instruksies" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Stel wagwoord terug" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Stuur weer bevestigings instruksies" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "Die kode vir verifikasie is gestuur na jou e-pos adres" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Wagwoordlose QRKode" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Kode is gestuur" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Jou wagwoord is verander." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "As jy nie jou wagwoord verander het nie," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "klik hier om dit te herstel" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" "As jy nie jou wagwoord verander het nie, klik die skakel hieronder om dit" " te herstel." #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "" #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Welkom %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Jy kan inteken op jou rekening deur die skakel hieronder:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Teken nou in" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "kan nie e-pos rekening bereik nie" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" #~ msgid "Or use the the link below:" #~ msgstr "Of gebruik die skakel hieronder:" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ "Gesamentlik met jou verbruikersnaam en " #~ "wagwoord moet jy 'n kode gebruik " #~ "wat ons vir jou sal stuur" #~ msgid "Please enter your authentication code" #~ msgstr "Sleutel asseblief jou verifikasie kode in" #~ msgid "Setup Unified Sign In options" #~ msgstr "" #~ msgid "Please re-authenticate" #~ msgstr "Herverifieer asseblief" #~ msgid "Please Enter Your Password" #~ msgstr "Sleutel Asseblief Jou Wagwoord In" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Geen wagwoord is gestel vir hierdie verbruiker nie" #~ msgid "Invalid Token" #~ msgstr "Ongeldige Token" #~ msgid "Your token has been confirmed" #~ msgstr "Jou token is bevestig" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "Jy het nie jou wagwoord terug " #~ "gestel binne %(within)s nie. Nuwe " #~ "instruksies is gestuur na %(email)s." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "Jy het nie jou e-pos adres " #~ "bevestig binne %(within)s nie. Nuwe " #~ "instruksies om jou e-pos adres te " #~ "bevestig is gestuur na %(email)s." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "Jy is nie geverifieer nie. Verskaf asseblief die korrekte getuigskrifte" #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ "Om intekening af te handel, sleutel " #~ "asseblief die kode in wat na jou" #~ " e-pos adres gestuur is" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "Na Watter Telefoon Nommer Moet Ons Die Kode Stuur?" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "'n E-pos is gestuur na ons om jou rekening te herstel" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "Dankie. Bevestigings instruksies is gestuur na %(email)s." #~ msgid "Two-factor Login" #~ msgstr "Twee-faktoor Intekening" #~ msgid "Two-factor Rescue" #~ msgstr "Twee-faktoor Redding" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Jy moet herverifieer om toegang te kry tot hierdie eindpunt" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "Jy het met sukses twee-faktoor verifikasie afgeskakel" #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "Twee-faktoor verifikasie voeg 'n ekstra laag sekuriteit by jou rekening" #~ msgid "Two factor authentication code" #~ msgstr "Twee-faktoor verifikasie kode" #~ msgid "Two-factor Authentication" #~ msgstr "Twee-faktoor Verifikasie" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Verander wagwoord" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Bevestig my rekening" #~ msgid "You can log into your account using the following code:" #~ msgstr "Jy kan inteken op jou rekening deur gebruik van die volgende kode:" #~ msgid "You can sign into your account using the following code:" #~ msgstr "Jy kan inteken op jou rekening deur gebruik van die volgende kode:" #~ msgid "Or use the link below:" #~ msgstr "Of gebruik die skakel hieronder:" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Bevestig asseblief jou e-pos adres deur die skakel hieronder:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Jy kan jou e-pos adres bevestig deur die skakel hieronder:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "Gebruik hierdie kode om in te teken: %(code)s." #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "Wagwoord vergeet" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider: (%(exerror)s -" #~ " %(exdesc)s). Please try again." #~ msgstr "" #~ msgid "Use Social Oauth to Sign In" #~ msgstr "" #~ msgid "Sign in with %(provider)s" #~ msgstr "" #~ msgid "Username contains illegal characters" #~ msgstr "" #~ msgid "Sign out" #~ msgstr "" #~ msgid "Click here to reset your password" #~ msgstr "Klik hier om jou wagwoord te herstel" #~ msgid "Click the link below to reset your password:" #~ msgstr "Klik die skakel hieronder om jou wagwoord te herstel:" #~ msgid "You can sign in to your account using the following code: %(token)s" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s." #~ msgstr "" pallets-eco-flask-security-72bf929/flask_security/translations/ar/000077500000000000000000000000001525721743500254205ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/ar/LC_MESSAGES/000077500000000000000000000000001525721743500272055ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/ar/LC_MESSAGES/flask_security.po000066400000000000000000001407311525721743500326020ustar00rootroot00000000000000# This file is distributed under the same license as the Flask-Security # project. # Sami Alfattany , 2025. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 5.8.0\n" "Report-Msgid-Bugs-To: jwag956@github.com\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: 2025-10-30 10:13+0200\n" "Last-Translator: Sami Alfattany \n" "Language: ar\n" "Language-Team: ar \n" "Plural-Forms: nplurals=3; plural=(n > 2);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "تأكد من بريدك الإلكتروني الجديد" #: flask_security/core.py:363 msgid "Login Required" msgstr "تسجيل الدخول مطلوب" #: flask_security/core.py:364 msgid "Welcome" msgstr "مرحبًا" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "يرجى تأكيد بريدك الإلكتروني" #: flask_security/core.py:366 msgid "Login instructions" msgstr "تعليمات تسجيل الدخول" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "تم إعادة تعيين كلمة المرور الخاصة بك" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "تم تغيير كلمة المرور الخاصة بك" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "تعليمات إعادة تعيين كلمة المرور" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "تم تغيير اسم المستخدم الخاص بك" #: flask_security/core.py:371 msgid "Your requested username" msgstr "اسم المستخدم الذي طلبته" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "تسجيل الدخول بخطوتين" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "استعادة التحقق بخطوتين" #: flask_security/core.py:423 msgid "Verification Code" msgstr "رمز التحقق" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "الإدخال غير مناسب لواجهة برمجة التطبيقات المطلوبة" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "�شل التحقق - الهوية أو كلمة المرور/الرمز غير صالح" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" "إذا كان هذا البريد الإلكتروني موجودًا �ي نظامنا، ستتلقى رسالة توضح كي�ية " "إعادة تعيين كلمة المرور الخاصة بك." #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "إذا كانت هذه الهوية موجودة �ي نظامنا، تم إرسال رمز إليك." #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "ليس لديك إذن لعرض هذا المورد." #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "يجب عليك تسجيل الدخول لعرض هذا المورد." #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "يجب عليك إعادة التحقق للوصول إلى هذه النقطة النهائية" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" "شكرًا لك. لتأكيد بريدك الإلكتروني %(email)s، يرجى النقر على الرابط �ي " "البريد الإلكتروني الذي أرسلناه لك." #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "شكرًا لك. تم تأكيد بريدك الإلكتروني." #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "تم تأكيد بريدك الإلكتروني بال�عل." #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "رمز التأكيد غير صالح." #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s مرتبط بال�عل بحساب." #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "سمة الهوية '%(attr)s' بالقيمة '%(value)s' مرتبطة بال�عل بحساب." #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "الهوية %(id)s غير مسجلة" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" "حدث خطأ أثناء التواصل مع مزود OAuth: (%(exerror)s - %(exdesc)s). يرجى " "المحاولة مرة أخرى." #: flask_security/core.py:531 msgid "Password does not match" msgstr "كلمة المرور غير متطابقة" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "كلمات المرور غير متطابقة" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "إعادة التوجيه خارج النطاق غير مسموح بها" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "رمز الاستعادة غير صالح" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "لم يتم إنشاء رموز استعادة بعد" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "تم إرسال تعليمات إعادة تعيين كلمة المرور إلى %(email)s." #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "لم تقم بإعادة تعيين كلمة المرور خلال %(within)s." #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "رمز إعادة تعيين كلمة المرور غير صالح." #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "البريد الإلكتروني يتطلب التأكيد." #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "تم إرسال تعليمات التأكيد إلى %(email)s." #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "لم تقم بتأكيد بريدك الإلكتروني خلال %(within)s." #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "لم تقم بتسجيل الدخول خلال %(within)s. تم إرسال تعليمات جديدة لتسجيل " "الدخول إلى %(email)s." #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "تم إرسال تعليمات تسجيل الدخول إلى %(email)s." #: flask_security/core.py:565 msgid "Invalid login token." msgstr "رمز تسجيل الدخول غير صالح." #: flask_security/core.py:566 msgid "Account is disabled." msgstr "تم تعطيل الحساب." #: flask_security/core.py:567 msgid "Email not provided" msgstr "لم يتم إدخال البريد الإلكتروني" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "عنوان البريد الإلكتروني غير صالح" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "رمز غير صالح" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "" #: flask_security/core.py:578 msgid "Password not provided" msgstr "لم يتم إدخال كلمة المرور" #: flask_security/core.py:580 #, python-format msgid "Password must be at least %(length)s characters" msgstr "يجب أن تتكون كلمة المرور من %(length)s أحر� على الأقل" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "كلمة المرور غير معقدة بما �يه الك�اية" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "كلمة المرور موجودة �ي قائمة مخترقة" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "�شل الاتصال بموقع كلمات المرور المخترقة" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "رقم الهات� غير صالح مثلًا رمز الدولة م�قود" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "المستخدم المحدد غير موجود" #: flask_security/core.py:591 msgid "Invalid password" msgstr "كلمة المرور غير صالحة" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "كلمة المرور أو الرمز المقدم غير صالح" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "تم تسجيل الدخول بنجاح." #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "هل نسيت كلمة المرور؟" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "تمت إعادة تعيين كلمة المرور بنجاح وتم تسجيل دخولك تلقائيًا." #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" "تمت إعادة تعيين كلمة المرور بنجاح. يرجى تسجيل الدخول باستخدام كلمة المرور" " الجديدة." #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "يجب أن تكون كلمة المرور الجديدة مختل�ة عن السابقة." #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "تم تغيير كلمة المرور بنجاح." #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "يرجى تسجيل الدخول للوصول إلى هذه الص�حة." #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "يرجى إعادة التحقق للوصول إلى هذه الص�حة." #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "تمت إعادة التحقق بنجاح" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "يمكنك الوصول إلى هذه النقطة �قط إذا لم تكن مسجل الدخول." #: flask_security/core.py:622 msgid "Code has been sent." msgstr "تم إرسال الرمز." #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "�شل إرسال الرمز. يرجى المحاولة لاحقًا" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "تم تأكيد الرمز الخاص بك" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "تم تغيير طريقة التحقق بخطوتين بنجاح." #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "ليس لديك حاليًا صلاحيات للوصول إلى هذه الص�حة" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "الطريقة المحددة غير صالحة" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "تم تعطيل التحقق بخطوتين بنجاح." #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "يجب إكمال الإعداد خلال %(within)s. يرجى البدء من جديد." #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "خيارات تسجيل الدخول النشطة حاليًا: %(method_list)s." #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "الطريقة المطلوبة غير صالحة" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "تم إعداد تسجيل الدخول الموحد بنجاح" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "يجب تحديد هوية صالحة لتسجيل الدخول" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "استخدم هذا الرمز لتسجيل الدخول: %(code)s" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "تم تغيير اسم المستخدم بنجاح" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "يجب أن يكون اسم المستخدم على الأقل %(min)d أحر� وأقل من %(max)d أحر�" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "يمكن أن يحتوي اسم المستخدم على أحر� وأرقام �قط" #: flask_security/core.py:667 msgid "Username not provided" msgstr "لم يتم إدخال اسم المستخدم" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "%(username)s مرتبط بال�عل بحساب." #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "يجب إكمال عملية م�تاح المرور خلال %(within)s. يرجى البدء من جديد." #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "يجب اختيار اسم للم�تاح الجديد." #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "%(name)s مرتبط بال�عل بم�تاح المرور ." #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "%(name)s غير مسجل مع المستخدم الحالي." #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "تم حذ� م�تاح م�تاح المرور بنجاح بالاسم: %(name)s" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "تمت إضا�ة م�تاح المرور بنجاح بالاسم: %(name)s" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "معر� م�تاح المرور مسجل بال�عل." #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "م�تاح المرور غير مسجل:" #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "م�تاح المرور لا ينتمي لأي مستخدم." #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "تعذر التحقق من م�تاح المرور: %(cause)s." #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "الم�تاح غير مسجل لهذا الاستخدام (أساسي أو ثانوي)" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "معر� مستخدم الم�تاح غير متطابق" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "يجب إكمال التأكيد خلال %(within)s. يرجى البدء من جديد." #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "تم تأكيد تغيير عنوان البريد الإلكتروني" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "تم إرسال تعليمات تأكيد بريدك الإلكتروني الجديد إلى %(email)s." #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "إذا كنت مسجلاً، سيتم إرسال اسم المستخدم إلى بريدك الإلكتروني." #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "الإعداد باستخدام تطبيق المصادقة (مثل Google أو LastPass أو Authy)" #: flask_security/forms.py:72 msgid "Change Method" msgstr "تغيير الطريقة" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "تغيير كلمة المرور" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "رمز التحقق" #: flask_security/forms.py:75 msgid "Delete" msgstr "حذ�" #: flask_security/forms.py:76 msgid "Email Address" msgstr "عنوان البريد الإلكتروني" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "الإعداد باستخدام البريد الإلكتروني" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "خطأ/أخطاء" #: flask_security/forms.py:79 msgid "Identity" msgstr "الهوية" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "تسجيل الدخول" #: flask_security/forms.py:81 msgid "New Password" msgstr "كلمة مرور جديدة" #: flask_security/forms.py:82 msgid "Passcode" msgstr "رمز المرور" #: flask_security/forms.py:83 msgid "Password" msgstr "كلمة المرور" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "رقم الهات�" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "استعادة كلمة المرور" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "استعادة اسم المستخدم" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "تسجيل" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "تذكرني" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "إعادة تعيين كلمة المرور" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "أعد كتابة كلمة المرور" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "إعادة إرسال تعليمات التأكيد" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "إرسال رابط تسجيل الدخول" #: flask_security/forms.py:93 msgid "Send Code" msgstr "إرسال الرمز" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "تسجيل الدخول" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "الإعداد باستخدام الرسائل النصية" #: flask_security/forms.py:96 msgid "Submit" msgstr "إرسال" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "إرسال الرمز" #: flask_security/forms.py:98 msgid "Username" msgstr "اسم المستخدم" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "تأكيد كلمة المرور" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "Google Authenticator" #: flask_security/forms.py:105 msgid "authenticator" msgstr "تطبيق المصادقة" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "البريد الإلكتروني" #: flask_security/forms.py:108 msgid "SMS" msgstr "رسائل نصية" #: flask_security/forms.py:109 msgid "password" msgstr "كلمة المرور" #: flask_security/forms.py:110 msgid "passkey" msgstr "م�تاح المرور" #: flask_security/forms.py:111 msgid "none" msgstr "لا شيء" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "الطرق المتاحة" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "تعطيل التحقق بخطوتين" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "هل تواجه مشكلة �ي الوصول إلى حسابك؟/�قدت جهازك المحمول؟" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "اتصل بالمسؤول" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "عرض رموز الاستعادة" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "إنشاء رموز استعادة جديدة" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "رمز الاستعادة" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "طرق التحقق الثانية المتاحة:" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "اختر" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "إرسال الرمز عبر البريد الإلكتروني" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "استخدم رمز الاستعادة الذي تم تحميله مسبقًا" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "رمز أو كلمة مرور" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "عبر البريد الإلكتروني" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "عبر الرسائل النصية" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "إعداد خيار تسجيل دخول إضا�ي" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "حذ� خيار تسجيل الدخول النشط" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "الاسم المستعار" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "الاستخدام" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "استخدم كعامل تحقق أول" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "استخدم كعامل تحقق ثانوي" #: flask_security/webauthn.py:241 msgid "Start" msgstr "ابدأ" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "القائمة" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "تغيير البريد الإلكتروني المسجل" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "تغيير اسم المستخدم" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "إعداد التحقق بخطوتين" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "إعداد تسجيل الدخول الموحد" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "إعدادات م�تاح المرور" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "تسجيل الدخول الموحد" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "تأكيد الحساب" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "تغيير البريد الإلكتروني" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "عند الإرسال، سيتم إرسال رسالة تأكيد إلى عنوان البريد الإلكتروني الجديد." #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "ليس لديك كلمة مرور حاليًا - سيتم إضا�ة واحدة." #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "اسم المستخدم الحالي هو: %(username)s" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "إرسال تعليمات إعادة تعيين كلمة المرور" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "أو" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "استخدم م�تاح المرور لتسجيل الدخول" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "تسجيل الدخول باستخدام م�تاح المرور" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "استمر مع %(provider)s" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "أدخل رمز الاستعادة" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "رموز الاستعادة" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" "تأكد من نسخ هذه الرموز وح�ظها �ي مكان آمن. يمكن استخدام كل رمز مرة واحدة " "�قط." #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "إنشاء رموز استعادة جديدة" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "استعادة اسم المستخدم" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "إعادة تعيين كلمة المرور" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "إعادة إرسال تعليمات التأكيد" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "اختر طريقة التحقق بخطوتين" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "التحقق بخطوتين يضي� طبقة أمان إضا�ية لحسابك" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "بالإضا�ة إلى اسم المستخدم وكلمة المرور، ستحتاج إلى استخدام رمز." #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "طريقة التحقق بخطوتين الحالية: %(method)s" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" "ا�تح تطبيق المصادقة على جهازك وامسح رمز QR التالي (أو أدخل الرمز يدويًا) " "لبدء استقبال الرموز:" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "رمز التحقق بخطوتين" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "أدخل الرمز لإكمال الإعداد" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "أدخل رمزًا رقميًا" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "يدعم هذا التطبيق إعداد رموز الاستعادة." #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "يمكنك إعدادها هنا." #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "م�اتيح المرور" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "يدعم هذا التطبيق م�اتيح المرور." #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "التحقق بخطوتين" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "يرجى إدخال رمز التحقق الذي تم إنشاؤه عبر: %(method)s" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "تم إرسال رمز التحقق إلى بريدك الإلكتروني" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "تم إرسال بريد إلكتروني إلينا لإعادة تعيين حساب التطبيق الخاص بك" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "إعداد تسجيل الدخول الموحد" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "رمز QR بدون كلمة مرور" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "لم يتم ت�عيل أي طرق - لا يوجد ما يمكن إعداده" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "أدخل الرمز هنا لإكمال الإعداد" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "طلب إرسال رمز لمرة واحدة" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "إعادة التحقق" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "تم إرسال الرمز" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "استخدم م�تاح المرور لإعادة التحقق" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "استخدم الدخول الاجتماعي لإعادة التحقق" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "إعداد م�تاح المرور جديد" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "ابدأ بتو�ير اسم �ريد لم�تاح المرور الجديد الخاص بك:" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "م�اتيح المرور المسجلة حاليًا:" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" "الاسم المستعار: \"%s\" الاستخدام: \"%s\" وسائل النقل: \"%s\" قابل " "للاكتشا�: \"%s\" نوع الجهاز: \"%s\" تم النسخ الاحتياطي؟ \"%s\" آخر " "استخدام �ي: %s" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "حذ� م�تاح المرور الحالي" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "تسجيل الدخول باستخدام المرور" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "تسجيل الدخول باستخدام المرور" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "استخدم م�تاح المرور كعامل تحقق ثانوي" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "إعادة التحقق باستخدام م�تاح المرور الخاص بك" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "استخدم هذا الرابط لتأكيد بريدك الإلكتروني الجديد." #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "سينتهي صلاحية هذا الرابط خلال %(within)s." #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "البريد الإلكتروني المسجل حاليًا هو %(email)s." #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "استخدم %(link)s لتأكيد بريدك الإلكتروني الجديد." #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "تم تغيير كلمة المرور الخاصة بك." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "إذا لم تقم بتغيير كلمة المرور الخاصة بك،" #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "انقر هنا لإعادة تعيينها" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" "إذا لم تقم بتغيير كلمة المرور الخاصة بك، انقر على الرابط أدناه لإعادة " "تعيينها." #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "تم تغيير اسم المستخدم الخاص بك." #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" "استخدم هذا الرابط لتأكيد بريدك " "الإلكتروني." #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "استخدم %(confirmation_link)s لتأكيد بريدك الإلكتروني." #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "مرحبًا %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "يمكنك تسجيل الدخول إلى حسابك من خلال الرابط أدناه:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "سجل الدخول الآن" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "مرحبًا %(username)s!" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "يمكنك تسجيل الدخول إلى حسابك باستخدام الرمز التالي: %(token)s" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "لا يمكن الوصول إلى حساب البريد الإلكتروني" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "أو استخدم هذا الرابط: تسجيل الدخول" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "أو استخدم هذا الرابط: %(login_link)s" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "مرحبًا،" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "لقد طلبت مؤخرًا استعادة اسم المستخدم الخاص بك." #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "اسم المستخدم الخاص بك هو: %(username)s" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "إذا لم تقم بطلب ذلك، يمكنك تجاهل هذا البريد الإلكتروني بأمان." #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "مرحبًا %(email)s!" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" "شخص ما (ربما أنت؟) حاول تسجيل هذا البريد الإلكتروني - وهو موجود بال�عل �ي" " نظامنا." #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "يرتبط بهذا الحساب أيضًا اسم المستخدم التالي: %(username)s." #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" "يمكنك استخدام هذا الرابط لإعادة تعيين كلمة" " المرور الخاصة بك." #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" "لم تقم بتأكيد بريدك الإلكتروني بعد - استخدم هذا الرابط للقيام بذلك الآن." #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "يرتبط بهذا الحساب أيضًا اسم المستخدم التالي: %(username)s" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" "يمكنك استخدام هذا الرابط %(reset_link)s لإعادة تعيين كلمة المرور الخاصة " "بك." #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" "لم تقم بتأكيد بريدك الإلكتروني بعد - استخدم هذا الرابط: " "%(confirmation_link)s للقيام بذلك الآن." #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "حاولت التسجيل باسم المستخدم \"%(username)s\" المرتبط بال�عل بحساب آخر." #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "يرجى إعادة بدء عملية التسجيل باسم مستخدم مختل�." #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "معر� م�تاح المرور غير مسجل." #~ msgid "webauthn" #~ msgstr "م�تاح المرور" #~ msgid "WebAuthn Setup" #~ msgstr "إعداد م�تاح المرور" #~ msgid "Forgot password" #~ msgstr "نسيت كلمة المرور" #~ msgid "WebAuthn" #~ msgstr "م�تاح المرور" #~ msgid "WebAuthn Security Key" #~ msgstr "م�تاح المرور" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "تسجيل الدخول باستخدام م�تاح المرور" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider: (%(exerror)s -" #~ " %(exdesc)s). Please try again." #~ msgstr "" #~ "حدث خطأ أثناء التواصل مع مزود " #~ "Oauth: (%(exerror)s - %(exdesc)s). يرجى " #~ "المحاولة مرة أخرى." #~ msgid "Use Social Oauth to Sign In" #~ msgstr "استخدم تسجيل الدخول الاجتماعي OAuth" #~ msgid "Sign in with %(provider)s" #~ msgstr "تسجيل الدخول باستخدام %(provider)s" #~ msgid "Username contains illegal characters" #~ msgstr "اسم المستخدم يحتوي على أحر� غير مسموح بها" #~ msgid "Sign out" #~ msgstr "تسجيل الخروج" #~ msgid "Click here to reset your password" #~ msgstr "انقر هنا لإعادة تعيين كلمة المرور الخاصة بك" #~ msgid "Click the link below to reset your password:" #~ msgstr "انقر على الرابط أدناه لإعادة تعيين كلمة المرور الخاصة بك:" #~ msgid "You can sign in to your account using the following code: %(token)s" #~ msgstr "يمكنك تسجيل الدخول إلى حسابك باستخدام الرمز التالي: %(token)s" #~ msgid "You can sign in to your account using the following code: %(token)s." #~ msgstr "يمكنك تسجيل الدخول إلى حسابك باستخدام الرمز التالي: %(token)s." pallets-eco-flask-security-72bf929/flask_security/translations/ar_SA/000077500000000000000000000000001525721743500260035ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/ar_SA/LC_MESSAGES/000077500000000000000000000000001525721743500275705ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/ar_SA/LC_MESSAGES/flask_security.po000066400000000000000000001407371525721743500331730ustar00rootroot00000000000000# This file is distributed under the same license as the Flask-Security # project. # Sami Alfattany , 2025. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 5.8.0\n" "Report-Msgid-Bugs-To: jwag956@github.com\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: 2025-10-30 10:13+0200\n" "Last-Translator: Sami Alfattany \n" "Language: ar_SA\n" "Language-Team: ar_SA \n" "Plural-Forms: nplurals=3; plural=(n > 2);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "تأكد من بريدك الإلكتروني الجديد" #: flask_security/core.py:363 msgid "Login Required" msgstr "تسجيل الدخول مطلوب" #: flask_security/core.py:364 msgid "Welcome" msgstr "مرحبًا" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "يرجى تأكيد بريدك الإلكتروني" #: flask_security/core.py:366 msgid "Login instructions" msgstr "تعليمات تسجيل الدخول" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "تم إعادة تعيين كلمة المرور الخاصة بك" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "تم تغيير كلمة المرور الخاصة بك" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "تعليمات إعادة تعيين كلمة المرور" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "تم تغيير اسم المستخدم الخاص بك" #: flask_security/core.py:371 msgid "Your requested username" msgstr "اسم المستخدم الذي طلبته" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "تسجيل الدخول بخطوتين" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "استعادة التحقق بخطوتين" #: flask_security/core.py:423 msgid "Verification Code" msgstr "رمز التحقق" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "الإدخال غير مناسب لواجهة برمجة التطبيقات المطلوبة" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "�شل التحقق - الهوية أو كلمة المرور/الرمز غير صالح" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" "إذا كان هذا البريد الإلكتروني موجودًا �ي نظامنا، ستتلقى رسالة توضح كي�ية " "إعادة تعيين كلمة المرور الخاصة بك." #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "إذا كانت هذه الهوية موجودة �ي نظامنا، تم إرسال رمز إليك." #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "ليس لديك إذن لعرض هذا المورد." #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "يجب عليك تسجيل الدخول لعرض هذا المورد." #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "يجب عليك إعادة التحقق للوصول إلى هذه النقطة النهائية" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" "شكرًا لك. لتأكيد بريدك الإلكتروني %(email)s، يرجى النقر على الرابط �ي " "البريد الإلكتروني الذي أرسلناه لك." #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "شكرًا لك. تم تأكيد بريدك الإلكتروني." #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "تم تأكيد بريدك الإلكتروني بال�عل." #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "رمز التأكيد غير صالح." #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s مرتبط بال�عل بحساب." #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "سمة الهوية '%(attr)s' بالقيمة '%(value)s' مرتبطة بال�عل بحساب." #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "الهوية %(id)s غير مسجلة" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" "حدث خطأ أثناء التواصل مع مزود OAuth: (%(exerror)s - %(exdesc)s). يرجى " "المحاولة مرة أخرى." #: flask_security/core.py:531 msgid "Password does not match" msgstr "كلمة المرور غير متطابقة" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "كلمات المرور غير متطابقة" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "إعادة التوجيه خارج النطاق غير مسموح بها" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "رمز الاستعادة غير صالح" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "لم يتم إنشاء رموز استعادة بعد" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "تم إرسال تعليمات إعادة تعيين كلمة المرور إلى %(email)s." #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "لم تقم بإعادة تعيين كلمة المرور خلال %(within)s." #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "رمز إعادة تعيين كلمة المرور غير صالح." #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "البريد الإلكتروني يتطلب التأكيد." #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "تم إرسال تعليمات التأكيد إلى %(email)s." #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "لم تقم بتأكيد بريدك الإلكتروني خلال %(within)s." #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "لم تقم بتسجيل الدخول خلال %(within)s. تم إرسال تعليمات جديدة لتسجيل " "الدخول إلى %(email)s." #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "تم إرسال تعليمات تسجيل الدخول إلى %(email)s." #: flask_security/core.py:565 msgid "Invalid login token." msgstr "رمز تسجيل الدخول غير صالح." #: flask_security/core.py:566 msgid "Account is disabled." msgstr "تم تعطيل الحساب." #: flask_security/core.py:567 msgid "Email not provided" msgstr "لم يتم إدخال البريد الإلكتروني" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "عنوان البريد الإلكتروني غير صالح" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "رمز غير صالح" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "" #: flask_security/core.py:578 msgid "Password not provided" msgstr "لم يتم إدخال كلمة المرور" #: flask_security/core.py:580 #, python-format msgid "Password must be at least %(length)s characters" msgstr "يجب أن تتكون كلمة المرور من %(length)s أحر� على الأقل" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "كلمة المرور غير معقدة بما �يه الك�اية" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "كلمة المرور موجودة �ي قائمة مخترقة" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "�شل الاتصال بموقع كلمات المرور المخترقة" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "رقم الهات� غير صالح مثلًا رمز الدولة م�قود" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "المستخدم المحدد غير موجود" #: flask_security/core.py:591 msgid "Invalid password" msgstr "كلمة المرور غير صالحة" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "كلمة المرور أو الرمز المقدم غير صالح" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "تم تسجيل الدخول بنجاح." #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "هل نسيت كلمة المرور؟" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "تمت إعادة تعيين كلمة المرور بنجاح وتم تسجيل دخولك تلقائيًا." #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" "تمت إعادة تعيين كلمة المرور بنجاح. يرجى تسجيل الدخول باستخدام كلمة المرور" " الجديدة." #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "يجب أن تكون كلمة المرور الجديدة مختل�ة عن السابقة." #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "تم تغيير كلمة المرور بنجاح." #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "يرجى تسجيل الدخول للوصول إلى هذه الص�حة." #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "يرجى إعادة التحقق للوصول إلى هذه الص�حة." #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "تمت إعادة التحقق بنجاح" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "يمكنك الوصول إلى هذه النقطة �قط إذا لم تكن مسجل الدخول." #: flask_security/core.py:622 msgid "Code has been sent." msgstr "تم إرسال الرمز." #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "�شل إرسال الرمز. يرجى المحاولة لاحقًا" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "تم تأكيد الرمز الخاص بك" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "تم تغيير طريقة التحقق بخطوتين بنجاح." #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "ليس لديك حاليًا صلاحيات للوصول إلى هذه الص�حة" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "الطريقة المحددة غير صالحة" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "تم تعطيل التحقق بخطوتين بنجاح." #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "يجب إكمال الإعداد خلال %(within)s. يرجى البدء من جديد." #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "خيارات تسجيل الدخول النشطة حاليًا: %(method_list)s." #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "الطريقة المطلوبة غير صالحة" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "تم إعداد تسجيل الدخول الموحد بنجاح" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "يجب تحديد هوية صالحة لتسجيل الدخول" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "استخدم هذا الرمز لتسجيل الدخول: %(code)s" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "تم تغيير اسم المستخدم بنجاح" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "يجب أن يكون اسم المستخدم على الأقل %(min)d أحر� وأقل من %(max)d أحر�" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "يمكن أن يحتوي اسم المستخدم على أحر� وأرقام �قط" #: flask_security/core.py:667 msgid "Username not provided" msgstr "لم يتم إدخال اسم المستخدم" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "%(username)s مرتبط بال�عل بحساب." #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "يجب إكمال عملية م�تاح المرور خلال %(within)s. يرجى البدء من جديد." #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "يجب اختيار اسم للم�تاح الجديد." #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "%(name)s مرتبط بال�عل بم�تاح المرور ." #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "%(name)s غير مسجل مع المستخدم الحالي." #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "تم حذ� م�تاح م�تاح المرور بنجاح بالاسم: %(name)s" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "تمت إضا�ة م�تاح المرور بنجاح بالاسم: %(name)s" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "معر� م�تاح المرور مسجل بال�عل." #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "م�تاح المرور غير مسجل:" #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "م�تاح المرور لا ينتمي لأي مستخدم." #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "تعذر التحقق من م�تاح المرور: %(cause)s." #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "الم�تاح غير مسجل لهذا الاستخدام (أساسي أو ثانوي)" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "معر� مستخدم الم�تاح غير متطابق" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "يجب إكمال التأكيد خلال %(within)s. يرجى البدء من جديد." #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "تم تأكيد تغيير عنوان البريد الإلكتروني" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "تم إرسال تعليمات تأكيد بريدك الإلكتروني الجديد إلى %(email)s." #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "إذا كنت مسجلاً، سيتم إرسال اسم المستخدم إلى بريدك الإلكتروني." #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "الإعداد باستخدام تطبيق المصادقة (مثل Google أو LastPass أو Authy)" #: flask_security/forms.py:72 msgid "Change Method" msgstr "تغيير الطريقة" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "تغيير كلمة المرور" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "رمز التحقق" #: flask_security/forms.py:75 msgid "Delete" msgstr "حذ�" #: flask_security/forms.py:76 msgid "Email Address" msgstr "عنوان البريد الإلكتروني" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "الإعداد باستخدام البريد الإلكتروني" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "خطأ/أخطاء" #: flask_security/forms.py:79 msgid "Identity" msgstr "الهوية" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "تسجيل الدخول" #: flask_security/forms.py:81 msgid "New Password" msgstr "كلمة مرور جديدة" #: flask_security/forms.py:82 msgid "Passcode" msgstr "رمز المرور" #: flask_security/forms.py:83 msgid "Password" msgstr "كلمة المرور" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "رقم الهات�" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "استعادة كلمة المرور" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "استعادة اسم المستخدم" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "تسجيل" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "تذكرني" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "إعادة تعيين كلمة المرور" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "أعد كتابة كلمة المرور" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "إعادة إرسال تعليمات التأكيد" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "إرسال رابط تسجيل الدخول" #: flask_security/forms.py:93 msgid "Send Code" msgstr "إرسال الرمز" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "تسجيل الدخول" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "الإعداد باستخدام الرسائل النصية" #: flask_security/forms.py:96 msgid "Submit" msgstr "إرسال" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "إرسال الرمز" #: flask_security/forms.py:98 msgid "Username" msgstr "اسم المستخدم" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "تأكيد كلمة المرور" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "Google Authenticator" #: flask_security/forms.py:105 msgid "authenticator" msgstr "تطبيق المصادقة" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "البريد الإلكتروني" #: flask_security/forms.py:108 msgid "SMS" msgstr "رسائل نصية" #: flask_security/forms.py:109 msgid "password" msgstr "كلمة المرور" #: flask_security/forms.py:110 msgid "passkey" msgstr "م�تاح المرور" #: flask_security/forms.py:111 msgid "none" msgstr "لا شيء" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "الطرق المتاحة" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "تعطيل التحقق بخطوتين" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "هل تواجه مشكلة �ي الوصول إلى حسابك؟/�قدت جهازك المحمول؟" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "اتصل بالمسؤول" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "عرض رموز الاستعادة" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "إنشاء رموز استعادة جديدة" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "رمز الاستعادة" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "طرق التحقق الثانية المتاحة:" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "اختر" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "إرسال الرمز عبر البريد الإلكتروني" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "استخدم رمز الاستعادة الذي تم تحميله مسبقًا" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "رمز أو كلمة مرور" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "عبر البريد الإلكتروني" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "عبر الرسائل النصية" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "إعداد خيار تسجيل دخول إضا�ي" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "حذ� خيار تسجيل الدخول النشط" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "الاسم المستعار" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "الاستخدام" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "استخدم كعامل تحقق أول" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "استخدم كعامل تحقق ثانوي" #: flask_security/webauthn.py:241 msgid "Start" msgstr "ابدأ" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "القائمة" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "تغيير البريد الإلكتروني المسجل" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "تغيير اسم المستخدم" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "إعداد التحقق بخطوتين" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "إعداد تسجيل الدخول الموحد" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "إعدادات م�تاح المرور" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "تسجيل الدخول الموحد" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "تأكيد الحساب" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "تغيير البريد الإلكتروني" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "عند الإرسال، سيتم إرسال رسالة تأكيد إلى عنوان البريد الإلكتروني الجديد." #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "ليس لديك كلمة مرور حاليًا - سيتم إضا�ة واحدة." #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "اسم المستخدم الحالي هو: %(username)s" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "إرسال تعليمات إعادة تعيين كلمة المرور" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "أو" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "استخدم م�تاح المرور لتسجيل الدخول" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "تسجيل الدخول باستخدام م�تاح المرور" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "استمر مع %(provider)s" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "أدخل رمز الاستعادة" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "رموز الاستعادة" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" "تأكد من نسخ هذه الرموز وح�ظها �ي مكان آمن. يمكن استخدام كل رمز مرة واحدة " "�قط." #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "إنشاء رموز استعادة جديدة" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "استعادة اسم المستخدم" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "إعادة تعيين كلمة المرور" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "إعادة إرسال تعليمات التأكيد" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "اختر طريقة التحقق بخطوتين" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "التحقق بخطوتين يضي� طبقة أمان إضا�ية لحسابك" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "بالإضا�ة إلى اسم المستخدم وكلمة المرور، ستحتاج إلى استخدام رمز." #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "طريقة التحقق بخطوتين الحالية: %(method)s" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" "ا�تح تطبيق المصادقة على جهازك وامسح رمز QR التالي (أو أدخل الرمز يدويًا) " "لبدء استقبال الرموز:" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "رمز التحقق بخطوتين" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "أدخل الرمز لإكمال الإعداد" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "أدخل رمزًا رقميًا" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "يدعم هذا التطبيق إعداد رموز الاستعادة." #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "يمكنك إعدادها هنا." #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "م�اتيح المرور" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "يدعم هذا التطبيق م�اتيح المرور." #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "التحقق بخطوتين" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "يرجى إدخال رمز التحقق الذي تم إنشاؤه عبر: %(method)s" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "تم إرسال رمز التحقق إلى بريدك الإلكتروني" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "تم إرسال بريد إلكتروني إلينا لإعادة تعيين حساب التطبيق الخاص بك" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "إعداد تسجيل الدخول الموحد" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "رمز QR بدون كلمة مرور" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "لم يتم ت�عيل أي طرق - لا يوجد ما يمكن إعداده" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "أدخل الرمز هنا لإكمال الإعداد" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "طلب إرسال رمز لمرة واحدة" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "إعادة التحقق" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "تم إرسال الرمز" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "استخدم م�تاح المرور لإعادة التحقق" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "استخدم الدخول الاجتماعي لإعادة التحقق" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "إعداد م�تاح المرور جديد" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "ابدأ بتو�ير اسم �ريد لم�تاح المرور الجديد الخاص بك:" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "م�اتيح المرور المسجلة حاليًا:" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" "الاسم المستعار: \"%s\" الاستخدام: \"%s\" وسائل النقل: \"%s\" قابل " "للاكتشا�: \"%s\" نوع الجهاز: \"%s\" تم النسخ الاحتياطي؟ \"%s\" آخر " "استخدام �ي: %s" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "حذ� م�تاح المرور الحالي" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "تسجيل الدخول باستخدام المرور" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "تسجيل الدخول باستخدام المرور" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "استخدم م�تاح المرور كعامل تحقق ثانوي" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "إعادة التحقق باستخدام م�تاح المرور الخاص بك" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "استخدم هذا الرابط لتأكيد بريدك الإلكتروني الجديد." #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "سينتهي صلاحية هذا الرابط خلال %(within)s." #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "البريد الإلكتروني المسجل حاليًا هو %(email)s." #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "استخدم %(link)s لتأكيد بريدك الإلكتروني الجديد." #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "تم تغيير كلمة المرور الخاصة بك." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "إذا لم تقم بتغيير كلمة المرور الخاصة بك،" #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "انقر هنا لإعادة تعيينها" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" "إذا لم تقم بتغيير كلمة المرور الخاصة بك، انقر على الرابط أدناه لإعادة " "تعيينها." #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "تم تغيير اسم المستخدم الخاص بك." #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" "استخدم هذا الرابط لتأكيد بريدك " "الإلكتروني." #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "استخدم %(confirmation_link)s لتأكيد بريدك الإلكتروني." #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "مرحبًا %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "يمكنك تسجيل الدخول إلى حسابك من خلال الرابط أدناه:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "سجل الدخول الآن" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "مرحبًا %(username)s!" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "يمكنك تسجيل الدخول إلى حسابك باستخدام الرمز التالي: %(token)s" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "لا يمكن الوصول إلى حساب البريد الإلكتروني" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "أو استخدم هذا الرابط: تسجيل الدخول" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "أو استخدم هذا الرابط: %(login_link)s" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "مرحبًا،" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "لقد طلبت مؤخرًا استعادة اسم المستخدم الخاص بك." #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "اسم المستخدم الخاص بك هو: %(username)s" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "إذا لم تقم بطلب ذلك، يمكنك تجاهل هذا البريد الإلكتروني بأمان." #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "مرحبًا %(email)s!" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" "شخص ما (ربما أنت؟) حاول تسجيل هذا البريد الإلكتروني - وهو موجود بال�عل �ي" " نظامنا." #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "يرتبط بهذا الحساب أيضًا اسم المستخدم التالي: %(username)s." #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" "يمكنك استخدام هذا الرابط لإعادة تعيين كلمة" " المرور الخاصة بك." #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" "لم تقم بتأكيد بريدك الإلكتروني بعد - استخدم هذا الرابط للقيام بذلك الآن." #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "يرتبط بهذا الحساب أيضًا اسم المستخدم التالي: %(username)s" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" "يمكنك استخدام هذا الرابط %(reset_link)s لإعادة تعيين كلمة المرور الخاصة " "بك." #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" "لم تقم بتأكيد بريدك الإلكتروني بعد - استخدم هذا الرابط: " "%(confirmation_link)s للقيام بذلك الآن." #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "حاولت التسجيل باسم المستخدم \"%(username)s\" المرتبط بال�عل بحساب آخر." #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "يرجى إعادة بدء عملية التسجيل باسم مستخدم مختل�." #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "معر� م�تاح المرور غير مسجل." #~ msgid "webauthn" #~ msgstr "م�تاح المرور" #~ msgid "WebAuthn Setup" #~ msgstr "إعداد م�تاح المرور" #~ msgid "Forgot password" #~ msgstr "نسيت كلمة المرور" #~ msgid "WebAuthn" #~ msgstr "م�تاح المرور" #~ msgid "WebAuthn Security Key" #~ msgstr "م�تاح المرور" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "تسجيل الدخول باستخدام م�تاح المرور" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider: (%(exerror)s -" #~ " %(exdesc)s). Please try again." #~ msgstr "" #~ "حدث خطأ أثناء التواصل مع مزود " #~ "Oauth: (%(exerror)s - %(exdesc)s). يرجى " #~ "المحاولة مرة أخرى." #~ msgid "Use Social Oauth to Sign In" #~ msgstr "استخدم تسجيل الدخول الاجتماعي OAuth" #~ msgid "Sign in with %(provider)s" #~ msgstr "تسجيل الدخول باستخدام %(provider)s" #~ msgid "Username contains illegal characters" #~ msgstr "اسم المستخدم يحتوي على أحر� غير مسموح بها" #~ msgid "Sign out" #~ msgstr "تسجيل الخروج" #~ msgid "Click here to reset your password" #~ msgstr "انقر هنا لإعادة تعيين كلمة المرور الخاصة بك" #~ msgid "Click the link below to reset your password:" #~ msgstr "انقر على الرابط أدناه لإعادة تعيين كلمة المرور الخاصة بك:" #~ msgid "You can sign in to your account using the following code: %(token)s" #~ msgstr "يمكنك تسجيل الدخول إلى حسابك باستخدام الرمز التالي: %(token)s" #~ msgid "You can sign in to your account using the following code: %(token)s." #~ msgstr "يمكنك تسجيل الدخول إلى حسابك باستخدام الرمز التالي: %(token)s." pallets-eco-flask-security-72bf929/flask_security/translations/ca_ES/000077500000000000000000000000001525721743500257705ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/ca_ES/LC_MESSAGES/000077500000000000000000000000001525721743500275555ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/ca_ES/LC_MESSAGES/flask_security.po000066400000000000000000001313111525721743500331440ustar00rootroot00000000000000# Catalan (Spain) translations for Flask-Security. # Copyright (C) 2017 DINSIC # This file is distributed under the same license as the Flask-Security # project. # Orestes Sanchez , 2018. # Ariel von Barnekow , 2025. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 3.1.0\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: 2026-08-21 12:02+0200\n" "Last-Translator: Ariel von Barnekow \n" "Language: ca_ES\n" "Language-Team: \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "Confirma la teva nova adreça de correu electrònic" #: flask_security/core.py:363 msgid "Login Required" msgstr "Cal iniciar la sessió" #: flask_security/core.py:364 msgid "Welcome" msgstr "Benvingut/da" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "Si us plau, confirmeu el vostre correu electrònic" #: flask_security/core.py:366 msgid "Login instructions" msgstr "Instruccions d'inici de la sessió" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "S'ha restablit la teva contrasenya" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "S'ha canviat la teva contrasenya" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "Instruccions de recuperació de la contrasenya" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "El teu nom d'usuari ha estat canviat" #: flask_security/core.py:371 msgid "Your requested username" msgstr "El nom d'usuari que has sol·licitat" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "Inici de sessió amb doble factor d'autenticació" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "Recuperació de doble factor" #: flask_security/core.py:423 msgid "Verification Code" msgstr "Codi de verificació" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "Dades d'entrada no apropiades per l'API demanada" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "Autenticació fallida - identitat o contrasenya/codi d'accés invàlid" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" "Si aquesta adreça de correu electrònic és al nostre sistema, rebràs un " "correu electrònic que descriu com restablir la teva contrasenya." #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "Si aquesta identitat és al nostre sistema, se t'ha enviat un codi." #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "No tens permís d'accés per a consultar aquest recurs." #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "Has d'iniciar sessió per veure aquest recurs." #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "Has de tornar a autenticar-te per accedir a aquest punt final" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" "Gràcies. Per confirmar la teva adreça de correu electrònic %(email)s, fes" " clic al enllaç del correu electrònic que acabem d'enviar-te." #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "Moltes gràcies. S'ha confirmat el teu correu electrònic." #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "El teu correu electrònic ja s'havia confirmat." #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "Token de confirmació no vàlid." #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s ja es associat amb un compte." #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" "L'atribut d'identitat '%(attr)s' amb valor '%(value)s' ja està associat a" " un compte." #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "Identitat %(id)s no registrada" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" "S'ha produït un error en comunicar-se amb el proveïdor OAuth: " "(%(exerror)s - %(exdesc)s). Si us plau, torneu-ho a provar." #: flask_security/core.py:531 msgid "Password does not match" msgstr "La contrasenya no coincideix" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "Les contrasenyes no coincideixen" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "Les redireccions a llocs web externes s'han prohibit" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "Codi de recuperació invàlid" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "Encara no s'han generat codis de recuperació" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "" "Les instruccions per restablir la teva contrasenya s'han enviat a " "%(email)s." #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "No has restablert la teva contrasenya dins de %(within)s. " #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "El token per restablir la contrasenya no és vàlid." #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "El correu electrònic requereix d'una confirmació." #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Les instruccions de confirmació s'han enviat a %(email)s." #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "No has confirmat el teu correu electrònic dins de %(within)s. " #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "No vas iniciar la sessió abans de %(within)s. S'han enviat noves " "instruccions a %(email)s." #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "S'han enviat instruccions per l'inici de sessió a %(email)s." #: flask_security/core.py:565 msgid "Invalid login token." msgstr "Token de d'inici de sessió no vàlid." #: flask_security/core.py:566 msgid "Account is disabled." msgstr "El compte està desactivat." #: flask_security/core.py:567 msgid "Email not provided" msgstr "No s'ha inclòs el correu electrònic" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "Adreça de correu electrònic no vàlida" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "Codi invàlid" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "El valor introduït no és vàlid per al camp" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "El valor introduït ha de ser inferior a %(length) caràcters" #: flask_security/core.py:578 msgid "Password not provided" msgstr "No s'ha inclòs la contrasenya" #: flask_security/core.py:580 #, python-format msgid "Password must be at least %(length)s characters" msgstr "La contrasenya ha de tenir al menys %(length)s caràcters" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "La contrasenya no és prou complexa" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "La contrasenya està a la llista de contrasenyes compromeses" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "No s'ha pogut contactar amb la web de filtracions de contrasenyes" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "Número de telèfon no vàlid, per exemple, falta el codi de país" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "L'usuari no existeix" #: flask_security/core.py:591 msgid "Invalid password" msgstr "Contrasenya no vàlida" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "La contrasenya o el codi enviat no és vàlid" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "La sessió s'ha iniciat amb èxit." #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "Has oblidat la teva contrasenya?" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" "Has restablert la teva contrasenya amb èxit i s'ha iniciat la sessió " "automàticament." #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" "Has restablert la teva contrasenya amb èxit. Si us plau, autentica't amb " "la teva nova contrasenya." #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "La nova contrasenya ha de ser diferent de l'anterior." #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "La teva contrasenya s'ha modificat amb èxit." #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "Has d'iniciar sessió per tal d'accedir a aquesta pàgina." #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "Has d'iniciar una nova sessió per tal d'accedir a aquesta pàgina." #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "El token de refresc és invàlid (%(reason)s)" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "Reautenticació exitosa" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "Només pots accedir a aquest punt final quan no estàs connectat." #: flask_security/core.py:622 msgid "Code has been sent." msgstr "S'ha enviat el codi." #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "No s'ha pogut enviar el codi. Si us plau, torna a intentar-ho més tard" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "El teu codi ha estat confirmat" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "Has canviat el teu mètode de doble factor amb èxit." #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "Actualment no tens permisos per accedir a aquesta pàgina" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "El mètode marcat no és vàlid" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "Has desactivat l'autenticació de doble factor amb èxit." #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" "La configuració s'ha de completar dins de %(within)s. Si us plau, torna a" " començar." #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "Opcions d'inici de sessió actualment actives: %(method_list)s." #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "El mètode sol·licitat no és vàlid" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "Configuració d'inici de sessió unificat exitosa" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "Heu de especificar una identitat vàlida per iniciar sessió" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "Utilitza aquest codi per iniciar sessió: %(code)s" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "Has canviat el teu nom d'usuari amb èxit" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" "El nom d'usuari ha de tenir almenys %(min)d caràcters i menys de %(max)d " "caràcters" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "El nom d'usuari només pot contenir lletres i números" #: flask_security/core.py:667 msgid "Username not provided" msgstr "Nom d'usuari no proporcionat" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "%(username)s ja està associat amb un compte." #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" "Les operacions de la clau d'accés s'han de completar dins de %(within)s. " "Si us plau, torna a començar." #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "Es requereix un sobrenom per a la nova clau d'accés." #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "%(name)s ja està associat amb una clau d'accés." #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "%(name)s no està registrat amb l'usuari actual." #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" "Els sobrenoms de la clau d'accés només poden contenir lletres, números i " "puntuació limitada." #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "S'ha eliminat la clau d'accés amb nom: %(name)s" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "S'ha afegit la clau d'accés amb nom: %(name)s" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "Clau d'accés ja registrada." #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "Clau d'accés no registrada." #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "La clau d'accés no pertany a cap usuari." #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "No s'ha pogut verificar la clau d'accés: %(cause)s." #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "Clau d'accés no registrada per a aquest ús (primera o secundària)" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "El controlador d'usuari de les credencials no coincideix" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" "La confirmació s'ha de completar dins de %(within)s. Si us plau, torna a " "començar." #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "Canvi d'adreça de correu electrònic confirmat" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" "Les instruccions per confirmar la teva nova adreça de correu electrònic " "s'han enviat a %(email)s." #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" "Si estàs registrat, el teu nom d'usuari s'enviarà al teu correu " "electrònic." #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" "Configura mitjançant una aplicació d'autenticació (per exemple: Google, " "LastPass, Authy)" #: flask_security/forms.py:72 msgid "Change Method" msgstr "Canviar el mètode" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Canvi de contrasenya" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "Codi d'autenticació" #: flask_security/forms.py:75 msgid "Delete" msgstr "Suprimeix" #: flask_security/forms.py:76 msgid "Email Address" msgstr "Correu electrònic" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "Configura mitjançant correu electrònic" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "Error(s)" #: flask_security/forms.py:79 msgid "Identity" msgstr "Identitat" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Iniciar sessió" #: flask_security/forms.py:81 msgid "New Password" msgstr "Nova contrasenya" #: flask_security/forms.py:82 msgid "Passcode" msgstr "Codi d'accés" #: flask_security/forms.py:83 msgid "Password" msgstr "Contrasenya" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "Número de telèfon" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "Restablir la contrasenya" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "Recupera el nom d'usuari" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Registrar-se" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "Recorda'm" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "Restablir la contrasenya" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "Escriu la contrasenya una altra vegada" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "Reenviar les instruccions de confirmació" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "Enviar l'enllaç d'inici de sessió" #: flask_security/forms.py:93 msgid "Send Code" msgstr "Envia codi" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Inicia la sessió" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "Configura mitjançant SMS" #: flask_security/forms.py:96 msgid "Submit" msgstr "Envia" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "Envia el codi" #: flask_security/forms.py:98 msgid "Username" msgstr "Nom d’usuari" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "Verifica la contrasenya" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "Google Authenticator" #: flask_security/forms.py:105 msgid "authenticator" msgstr "autenticador" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "correu electrònic" #: flask_security/forms.py:108 msgid "SMS" msgstr "SMS" #: flask_security/forms.py:109 msgid "password" msgstr "contrasenya" #: flask_security/forms.py:110 msgid "passkey" msgstr "clau d'accés" #: flask_security/forms.py:111 msgid "none" msgstr "cap" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "Token de refresc" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "Mètodes disponibles" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "Desactiva l'autenticació de doble factor" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "Tens problemes per accedir al teu compte? / Dispositiu mòbil perdut?" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "Contacta amb l'administrador" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "Mostra els codis de recuperació" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "Genera nous codis de recuperació" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "Codi de recuperació" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "Mètodes disponibles per al doble factor d'autenticació:" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "Selecciona" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "Envia el codi per correu electrònic" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "Utilitza el codi de recuperació descarregat anteriorment" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "Codi o contrasenya" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "Via correu electrònic" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "Via SMS" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "Configura una opció d'inici de sessió addicional" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "Elimina l'opció d'inici de sessió activa" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "Sobrenom" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "Ús" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "Utilitza com a primer factor d'autenticació" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "Utilitza com a segon factor d'autenticació" #: flask_security/webauthn.py:241 msgid "Start" msgstr "Comença" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menú" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "Canvi del correu electrònic registrat" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "Canvi del nom d'usuari" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "Configuració de doble factor" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "Configuració d'inici de sessió unificat" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "Configuració de clau d'accés" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "Inici de sessió unificat" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "Confirmació de compte" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "Canvi de correu electrònic" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" "Un cop enviat, es enviarà una confirmació per correu electrònic a aquesta" " nova adreça de correu electrònic." #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "Actualment no tens una contrasenya - això n'afegirà una." #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "El nom d'usuari actual és: %(username)s" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Enviar instruccions per restablir la contrasenya" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "o" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "Utilitza una clau d'accés per iniciar sessió" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "Inicia sessió amb una clau d'accés" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "Utilitza el registre social per iniciar sessió" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "Continua amb %(provider)s" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "Confirma la desconnexió" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "Introdueix el codi de recuperació" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "Codis de recuperació" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" "Assegura't de copiar-los i emmagatzemar-los en un lloc segur. Cada codi " "només es pot utilitzar una vegada." #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "Genera nous codis de recuperació" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "Recuperació d'usuari" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Restablir la contrasenya" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Reenviar instruccions de confirmació" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "Selecciona el mètode de doble factor" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" "L'autenticació de doble factor afegeix una capa addicional de seguretat " "al teu compte" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "A més del teu nom d'usuari i contrasenya, necessitaràs utilitzar un codi." #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "Mètode de doble factor actualment configurat: %(method)s" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" "Obre una aplicació d'autenticació al teu dispositiu i escaneja el següent" " codi QR (o introdueix el codi manualment a continuació) per començar a " "rebre codis:" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "Codi d'autenticació de doble factor" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "Introdueix el codi per completar la configuració" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "introdueix el codi numèric" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "Aquesta aplicació admet la configuració de codis de recuperació." #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "Pots configurar-los aquí." #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "Claus d'accés" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "Aquesta aplicació admet claus d'accés." #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "Doble factor d'autenticació" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" "Si us plau, introdueix el teu codi d'autenticació generat a través de: " "%(method)s" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "El codi d'autenticació s'ha enviat a la teva adreça de correu electrònic" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "S'ha enviat un correu electrònic per restablir el teu compte d'aplicació" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "Configuració de l'inici de sessió unificat" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Codi QR sense contrasenya" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "No s'han habilitat mètodes - res per configurar" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "Introdueix el codi aquí per completar la configuració" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "Sol·licita que s'enviï un codi d'un sol ús" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "Tornat a identificar" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "El codi s'ha enviat" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "Utilitza una clau d'accés per reautenticar" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "Utilitza el registre social per tornar a autenticar-vos" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "Configura una nova clau d'accés" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "Comença proporcionant un nom únic per a la teva clau d'accés:" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "Claus d'accés actualment registrades:" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" "Àlies: \"%s\" Ús: \"%s\" Transports: \"%s\" Descobrible: \"%s\" Tipus de " "dispositiu: \"%s\" Copiat? \"%s\" Últim utilitzat el: %s" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "Elimina una clau d'accés existent" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "Inicia la sessió amb una clau d'accés" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "Inicia la sessió amb una clau d'accés" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "Utilitza una clau de seguretat com a doble factor" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "Tornat a identificar utilitzant una clau d'accés" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" "Utilitza aquest enllaç per confirmar la teva " "nova adreça de correu electrònic." #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "Aquest enllaç caducarà en %(within)s." #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "La teva adreça de correu electrònic registrada actualment és %(email)s." #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "Utilitza %(link)s per confirmar la teva nova adreça de correu electrònic." #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "S'ha canviat la teva contrasenya." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Si no has canviat la teva contrasenya," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "fes clic aquí per a restablir-la" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" "Si no has canviat la teva contrasenya, fes clic a l'enllaç següent per " "restablir-la." #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "El teu nom d'usuari ha estat canviat." #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" "Utilitza aquest enllaç per " "confirmar la teva adreça de correu electrònic." #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "" "Utilitza %(confirmation_link)s per confirmar la teva adreça de correu " "electrònic." #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Benvingut %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Inicia la sessió fent clic aquí:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Iniciar sessió ara" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" "Utilitza aquest enllaç per restablir la " "teva contrasenya." #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "Utilitza %(reset_link)s per restablir la teva contrasenya." #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "La teva contrasenya ha estat restablerta." #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "Benvingut %(username)s!" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "Pots iniciar la sessió al teu compte utilitzant el següent codi: %(token)s" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "Aquest codi caducarà en %(within)s." #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "no es pot accedir al compte de correu" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "Pots iniciar sessió al teu compte utilitzant el següent codi: %(code)s" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "O utilitza aquest enllaç: Inicia la sessió" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "Aquest codi/enllaç caducarà en %(within)s." #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "O utilitza aquest enllaç: %(login_link)s" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "Hola," #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "Recentment has sol·licitat recuperar el teu nom d'usuari." #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "El teu nom d'usuari és: %(username)s" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" "Si no has iniciat aquesta sol·licitud, pots ignorar aquest correu " "electrònic." #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "Hola %(email)s!" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" "Algú (tu?) ha intentat registrar aquest correu electrònic - que ja està " "al nostre sistema." #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "Aquest compte també té el següent nom d'usuari associat: %(username)s." #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" "Pots utilitzar aquest enllaç per restablir" " la teva contrasenya." #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" "Encara no has confirmat la teva adreça de correu electrònic - utilitza aquest enllaç per fer-ho ara." #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "Aquest compte també té el següent nom d'usuari associat: %(username)s" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" "Pots utilitzar aquest enllaç %(reset_link)s per restablir la teva " "contrasenya." #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" "Encara no has confirmat la teva adreça de correu electrònic - utilitza " "aquest enllaç: %(confirmation_link)s per fer-ho ara." #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" "Has intentat registrar-te amb un nom d'usuari \"%(username)s\" que ja " "està associat amb un altre compte." #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "Si us plau, reinicia el procés de registre amb un nom d'usuari diferent." #~ msgid "Username contains illegal characters" #~ msgstr "El nom d'usuari conté caràcters il·legals" #~ msgid "Sign out" #~ msgstr "Tanca la sessió" #~ msgid "Click here to reset your password" #~ msgstr "Feu clic aquí per restablir la contrasenya" #~ msgid "Click the link below to reset your password:" #~ msgstr "Fes clic a l'enllaç següent per restablir la teva contrasenya:" #~ msgid "You can sign in to your account using the following code: %(token)s" #~ msgstr "" #~ "Pots iniciar la sessió al teu " #~ "compte utilitzant el següent codi: " #~ "%(token)s" #~ msgid "You can sign in to your account using the following code: %(token)s." #~ msgstr "" #~ "Pots iniciar sessió al teu compte " #~ "utilitzant el següent codi: %(token)s." pallets-eco-flask-security-72bf929/flask_security/translations/da_DK/000077500000000000000000000000001525721743500257605ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/da_DK/LC_MESSAGES/000077500000000000000000000000001525721743500275455ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/da_DK/LC_MESSAGES/flask_security.po000066400000000000000000001217211525721743500331400ustar00rootroot00000000000000# Danish (Denmark) translations for Flask-Security. # Copyright (C) 2017 ORGANIZATION # This file is distributed under the same license as the Flask-Security # project. # FIRST AUTHOR , 2017. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 2.1.0\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: 2017-03-23 14:04+0100\n" "Last-Translator: Leonhard Printz \n" "Language: da_DK\n" "Language-Team: da_DK \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:363 msgid "Login Required" msgstr "Login påkræveet" #: flask_security/core.py:364 msgid "Welcome" msgstr "Velkommen" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "Bekræft venligst din email" #: flask_security/core.py:366 msgid "Login instructions" msgstr "Logininstruktioner" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "Din adgangskode er blevet nulstillet" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "Din adgangskode er blevet ændret" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "Instruktioner til nulstilling af adganskode" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:371 msgid "Your requested username" msgstr "" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:423 msgid "Verification Code" msgstr "" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "Du har ikke adgang til denne resource." #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "Mange Tak. Din email er blevet bekræftet." #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "Din email er allerede blevet bekræftet." #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "Ugyldig bekræftigelsestoken." #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s er allerede brugt af en anden konto." #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:531 msgid "Password does not match" msgstr "Adgangskode passer ikke" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "Adgangskoderne passer ikke" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "Omdirigering udenfor domænet er forbudt" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "" "Instruktioner til nulstilling af din adgangskode er blevet sendt til " "%(email)s." #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "Ugyldig nulstillingstoken." #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "Email kræver bekræftigelse." #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Bekræftigelsesinstruktioner er blevet sendt til %(email)s." #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Du har ikke logget in indenfor %(within)s. Nye logininstruktioner er " "blevet sendt til %(email)s." #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Logininstruktioner er blevet sendt til %(email)s." #: flask_security/core.py:565 msgid "Invalid login token." msgstr "Ugyldig logintoken." #: flask_security/core.py:566 msgid "Account is disabled." msgstr "Kontoen er deaktiveret." #: flask_security/core.py:567 msgid "Email not provided" msgstr "Email ikke angivet" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "Ugyldig email adresse" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "" #: flask_security/core.py:578 msgid "Password not provided" msgstr "Adgangskode ikke angivet" #: flask_security/core.py:580 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" msgstr "Adgangskoden skal indeholde mindst %(length)s tegn" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "Denne bruger findes ikke" #: flask_security/core.py:591 msgid "Invalid password" msgstr "Ugyldig adgangskode" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "Du er hermed blevet logget ind." #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "Glemt adgangskode?" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" "Du har hermed nulstillet din adgangskode og er blevet automatisk logget " "ind." #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "Din nye adgangskode skal være anderledes end din tidligere adgangskode." #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "Du har hermed ændret din adgangskode." #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "Log in for at få adgang til denne side." #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "Bekræft identitet for at få adgang til denne side." #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "" #: flask_security/core.py:622 msgid "Code has been sent." msgstr "" #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "" #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:667 msgid "Username not provided" msgstr "" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" #: flask_security/forms.py:72 msgid "Change Method" msgstr "" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Ændre adgangskode" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "" #: flask_security/forms.py:75 msgid "Delete" msgstr "" #: flask_security/forms.py:76 msgid "Email Address" msgstr "Email adresse" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "" #: flask_security/forms.py:79 msgid "Identity" msgstr "" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Login" #: flask_security/forms.py:81 msgid "New Password" msgstr "Ny adgangskode" #: flask_security/forms.py:82 msgid "Passcode" msgstr "" #: flask_security/forms.py:83 msgid "Password" msgstr "Adgangskode" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "Genopret adgangskode" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Registrer" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "Husk" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "Nulstil adgangskode" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "Gentast adgangskode" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "Gensend bekræftelsesinstruktioner" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "Send login link" #: flask_security/forms.py:93 msgid "Send Code" msgstr "" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "" #: flask_security/forms.py:96 msgid "Submit" msgstr "" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "" #: flask_security/forms.py:98 msgid "Username" msgstr "" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:105 msgid "authenticator" msgstr "" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "" #: flask_security/forms.py:108 msgid "SMS" msgstr "" #: flask_security/forms.py:109 msgid "password" msgstr "" #: flask_security/forms.py:110 msgid "passkey" msgstr "" #: flask_security/forms.py:111 msgid "none" msgstr "" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:241 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menu" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "Bekræft konto" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Send adgangskode nulstillingsinstruktioner" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Nulstil adgangskode" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Gensend bekræftelsesinstruktioner" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Din adgangskode er blevet ændret." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Hvis du ikke har ændret din adgangskode," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "klik her for at ændre den" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "" #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Velkommen %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Du kan logge ind gennem nedenstående link:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Login" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" #~ msgid "You successfully confirmed password" #~ msgstr "" #~ msgid "Password confirmation is needed in order to access page" #~ msgstr "" #~ msgid "" #~ "Open your authenticator app on your " #~ "device and scan the following qrcode " #~ "to start receiving codes:" #~ msgstr "" #~ msgid "Or use the the link below:" #~ msgstr "" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ msgid "Please enter your authentication code" #~ msgstr "" #~ msgid "Setup Unified Sign In options" #~ msgstr "" #~ msgid "Please re-authenticate" #~ msgstr "" #~ msgid "Please Enter Your Password" #~ msgstr "" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Denne bruger har ingen adganskode" #~ msgid "Invalid Token" #~ msgstr "" #~ msgid "Your token has been confirmed" #~ msgstr "" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "Du har ikke nulstillet din adgangskode" #~ " indenfor %(within)s. Nye instruktioner er" #~ " sendt til %(email)s." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "Du har ikke bekræftet din email " #~ "indenfor %(within)s. Nye instruktioner er " #~ "blevet sendt til %(email)s." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "" #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "Mange tak. Bekræftelsesinstruktioner er blevet sendt til %(email)s." #~ msgid "Two-factor Login" #~ msgstr "" #~ msgid "Two-factor Rescue" #~ msgstr "" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "" #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ msgid "Two factor authentication code" #~ msgstr "" #~ msgid "Two-factor Authentication" #~ msgstr "" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Ændre adgangskode" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Bekræft ny konto" #~ msgid "You can log into your account using the following code:" #~ msgstr "" #~ msgid "You can sign into your account using the following code:" #~ msgstr "" #~ msgid "Or use the link below:" #~ msgstr "" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Bekræft venligst din email gennem nedenstående link:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Bekræft venligst din email gennem nedenstående link:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "Glemt din adgangskode" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider: (%(exerror)s -" #~ " %(exdesc)s). Please try again." #~ msgstr "" #~ msgid "Use Social Oauth to Sign In" #~ msgstr "" #~ msgid "Sign in with %(provider)s" #~ msgstr "" #~ msgid "Username contains illegal characters" #~ msgstr "" #~ msgid "Sign out" #~ msgstr "" #~ msgid "Click here to reset your password" #~ msgstr "Klik her for at nulstille din adgangskode" #~ msgid "Click the link below to reset your password:" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s." #~ msgstr "" pallets-eco-flask-security-72bf929/flask_security/translations/de_DE/000077500000000000000000000000001525721743500257565ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/de_DE/LC_MESSAGES/000077500000000000000000000000001525721743500275435ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/de_DE/LC_MESSAGES/flask_security.po000066400000000000000000001541401525721743500331370ustar00rootroot00000000000000# German translation for Flask-Security # Copyright (C) 2017-2025 THE AUTHORS # This file is distributed under the same license as the Flask-Security # project. # Ingo Kleiber , 2017, # Erich Seifert , 2017. # Pascua Theus , 2021-2022 # Stephan Wurm , 2025 # msgid "" msgstr "" "Project-Id-Version: Flask-Security 5.7.0\n" "Report-Msgid-Bugs-To: jwag956@github.com\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: 2022-04-05 13:50+0200\n" "Last-Translator: Stephan Wurm \n" "Language: de_DE\n" "Language-Team: de_DE \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "Bestätigen Sie Ihre neue E-Mail-Adresse" #: flask_security/core.py:363 msgid "Login Required" msgstr "Anmeldung erforderlich" #: flask_security/core.py:364 msgid "Welcome" msgstr "Willkommen" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "Bitte E-Mail-Adresse bestätigen" #: flask_security/core.py:366 msgid "Login instructions" msgstr "Anmeldeanleitung" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "Das Passwort wurde zurückgesetzt" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "Das Passwort wurde geändert" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "Anleitung zur Passwortwiederherstellung" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "Der Benutzername wurde geändert" #: flask_security/core.py:371 msgid "Your requested username" msgstr "Der gewählte Benutzername" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "Zwei-Faktor-Anmeldung" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "Zwei-Faktor-Wiederherstellung" #: flask_security/core.py:423 msgid "Verification Code" msgstr "Verifizierungscode" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "Ungültige Eingabe für die angeforderte Ressource" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" "Authentifizierung fehlgeschlagen – Identität oder Passwort/Passcode " "ungültig" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" "Wenn diese E-Mail-Adresse bei uns existiert, erhalten Sie eine E-Mail, in" " der beschrieben wird, wie Sie Ihr Passwort zurücksetzen können." #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "" "Wenn uns diese Identität bekannt ist, wird Ihnen ein Einmalpasswort " "zugesandt." #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "Sie haben keine Berechtigung, um diese Ressource zu sehen." #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "Sie müssen sich anmelden, um diese Ressource zu sehen." #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "Sie müssen sich neu anmelden um auf diesen Endpunkt zugreifen zu können" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" "Vielen Dank. Um Ihre E-Mail-Adresse %(email)s zu bestätigen, klicken Sie " "bitte auf den Link in der E-Mail, die wir gerade an Sie gesendet haben." #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "Vielen Dank. Die E-Mail-Adresse wurde bestätigt." #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "Die E-Mail-Adresse wurde bereits bestätigt." #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "Ungültiger Bestätigungscode." #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s ist bereits mit einem Konto verknüpft." #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" "Benutzermerkmal '%(attr)s' mit Wert '%(value)s' ist bereits mit einem " "anderen Benutzerkonto verknüpft" #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "Benutzer %(id)s nicht registriert" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:531 msgid "Password does not match" msgstr "Das Passwort stimmt nicht überein" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "Die Passwörter stimmen nicht überein" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "Weiterleitungen außerhalb der Domain sind verboten" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "Wiederherstellungscode ungültig" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "Es wurden noch keine Sicherheitscodes generiert." #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "" "Eine Anleitung, um das Passwort wiederherzustellen wurde an %(email)s " "gesendet." #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "Sie haben ihr Passwort nicht innerhalb von %(within)s zurückgesetzt. " #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "Ungültiger Passwortwiederherstellungscode." #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "Die E-Mail-Adresse muss bestätigt werden." #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "" "Um Ihre E-Mail-Adresse %(email)s zu bestätigen, klicken Sie bitte auf den" " Link in der E-Mail, die wir gerade an Sie gesendet haben." #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "Sie haben Ihre E-Mail-Adresse nich innerhalb von %(within)s bestätigt. " #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Die Anmeldung erfolgte nicht innerhalb von %(within)s. Eine neue " "Anleitung wurde an %(email)s gesendet." #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Eine Anleitung zur Anmeldung wurde an %(email)s gesendet." #: flask_security/core.py:565 msgid "Invalid login token." msgstr "Ungültiger Anmeldecode." #: flask_security/core.py:566 msgid "Account is disabled." msgstr "Konto ist deaktiviert." #: flask_security/core.py:567 msgid "Email not provided" msgstr "Keine E-Mail-Adresse angegeben" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "Ungültige E-Mail-Adresse" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "Ungültiger Code" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "" #: flask_security/core.py:578 msgid "Password not provided" msgstr "Kein Passwort angegeben" #: flask_security/core.py:580 #, python-format msgid "Password must be at least %(length)s characters" msgstr "Das Passwort muss mindestens %(length)s Zeichen lang sein" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "Passwort ist nicht komplex genug" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "Passwort ist öffentlich bekannt" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "Konnte keine Verbindung zum Dienst aufbauen, um Passwörter zu überprüfen." #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "Telefonnumer ist ungültig, eventuell fehlt die Landesvorwahl" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "Der angegebene Benutzer existiert nicht" #: flask_security/core.py:591 msgid "Invalid password" msgstr "Ungültiges Passwort" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "Übermitteltes Passwort oder Code ist ungültig" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "Sie wurden angemeldet." #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "Passwort vergessen?" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" "Das Passwort wurde erfolgreich wiederhergestellt und die Anmeldung " "erfolgte automatisch." #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" "Sie haben Ihr Passwort erfolgreich zurückgesetzt. Bitte bestätigen Sie " "Ihr neues Passwort." #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "Das neue Passwort muss sich vom vorherigen unterscheiden." #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "Das Passwort wurde erfolgreich geändert." #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "Bitte melden Sie sich an, um diese Seite zu sehen." #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "Bitte neu anmelden, um auf diese Seite zuzugreifen." #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "Neuanmeldung erfolgreich" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "Dieser Endpunkt ist nur für angemeldete Nutzer erlaubt." #: flask_security/core.py:622 msgid "Code has been sent." msgstr "Einmalpasswort wurde gesendet." #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "" "Zusendung des Einmalpassworts fehlgeschlagen. Bitte versuchen Sie es " "später erneut" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "Ihr Einmalpasswort wurde bestätigt" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "Sie haben Ihre Zwei-Faktor-Methode erfolgreich geändert." #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "Sie haben aktuell nicht die nötigen Rechte, um die Seite anzusehen" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "Ausgewählte Methode ist nicht gültig" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "Die Zwei-Faktor-Authentisierung wurde erfolgreich deaktiviert." #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" "Die Einrichtung muss innerhalb von %(within)s abgeschlossen werden. Bitte" " erneut versuchen." #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "Mögliche Anmeldeoptionen: %(method_list)s." #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "Angefragte Methode ist ungültig" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "Single-User-Login erfolgreich eingerichtet" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "Sie müssen eine gültige Identität auswählen, um sich anzumelden" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "Verwenden Sie das folgende Einmalpasswort zur Anmeldung: %(code)s" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "Benutzername erfolgreich geändert" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" "Der Benutzername muss mindestens %(min)d und darf nicht länger als " "%(max)d Zeichen sein." #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "Der Benutzername darf nur aus Buchstaben und Ziffern bestehen" #: flask_security/core.py:667 msgid "Username not provided" msgstr "Benutzername nicht angegeben" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "%(username)s ist bereits mit einem Benutzerkonto verknüpft." #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" "Der Anmeldevorgang per Passkey muss innerhalb von %(within)s " "abgeschlossen sein. Bitte erneut versuchen." #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "Für den neuen Passkey ist ein eindeutiger Bezeichner erforderlich." #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "%(name)s ist bereits mit einem anderen Passkey verknüpft." #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "%(name)s ist für den aktuellen Benutzer nicht eingetragen." #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "Passkey erfolgreich gelöscht: %(name)s" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "Passkey erfolgreich eingetragen: %(name)s" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "Der Passkey ist bereits eingetragen." #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "Nicht eingetragener Passkey." #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "Der Passkey ist mit keinem Benutzerkonto verknüpft." #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "Konnte den Passkey nicht verifizieren: %(cause)s." #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "" "Der Passkey wurde für diesen Anwendungsfall (erster oder zweiter Faktor) " "nicht eingetragen" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "Passkey passt nicht zum Benutzer." #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" "Die Bestätigung muss innerhalb von %(within)s erfolgen. Bitte erneut " "versuchen." #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "Änderung der E-Mail-Adresse bestätigt" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" "Eine Anleitung zur Bestätigung der neuen E-Mail-Adresse wurde an " "%(email)s gesendet." #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" "Falls bekannt, wird Ihr Benutzername an die hinterlegte E-Mail-Adresse " "gesendet." #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "Einrichtung via Authentisierungs-App" #: flask_security/forms.py:72 msgid "Change Method" msgstr "Methode ändern" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Passwort ändern" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "Authentisierungscode" #: flask_security/forms.py:75 msgid "Delete" msgstr "Löschen" #: flask_security/forms.py:76 msgid "Email Address" msgstr "E-Mail-Adresse" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "Einrichtung via E-Mail" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "Fehler" #: flask_security/forms.py:79 msgid "Identity" msgstr "Identität" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Anmelden" #: flask_security/forms.py:81 msgid "New Password" msgstr "Neues Passwort" #: flask_security/forms.py:82 msgid "Passcode" msgstr "Anmelde-Code" #: flask_security/forms.py:83 msgid "Password" msgstr "Passwort" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "Telefonnummer" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "Passwort wiederherstellen" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "Benutzername wiederherstellen" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Registrieren" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "Erinnern" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "Passwort zurücksetzen" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "Passwort erneut eingeben" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "Bestätigungslink erneut senden" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "Anmelde-Link versenden" #: flask_security/forms.py:93 msgid "Send Code" msgstr "Einmalpasswort versenden" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Anmeldung" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "Einrichtung via SMS" #: flask_security/forms.py:96 msgid "Submit" msgstr "Ausführen" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "Mit Authentisierungscode bestätigen" #: flask_security/forms.py:98 msgid "Username" msgstr "Benutzername" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "Password bestätigen" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:105 msgid "authenticator" msgstr "Authentisierungs-App" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "E-Mail" #: flask_security/forms.py:108 msgid "SMS" msgstr "" #: flask_security/forms.py:109 msgid "password" msgstr "Passwort" #: flask_security/forms.py:110 msgid "passkey" msgstr "Passkey" #: flask_security/forms.py:111 msgid "none" msgstr "keine" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "Verfügbare Methoden" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "Deaktiviere Zwei-Faktor-Authentisierung" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "Probleme beim Zugriff auf Ihr Benutzerkonto? Smartphone verloren?" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "Kontaktiere einen Administrator" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "Wiederherstellungscodes anzeigen" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "Erzeuge neue Wiederherstellungscodes" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "Wiederherstellungscode" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "Verfügbare Zwei-Faktor-Methoden:" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "Auswählen" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "Sende Einmalpasswort per E-Mail" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "Zuvor heruntergeladenen Wiederherstellungscode verwenden" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "Einmalpasswort oder Passwort" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "Via E-Mail" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "Via SMS" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "Richte weitere Single-User-Login-Option ein" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "Lösche aktivierte Anmeldeoption" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "Bezeichner" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "Verwendung" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "Als ersten Faktor verwenden" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "Als zweiten Faktor verwenden" #: flask_security/webauthn.py:241 msgid "Start" msgstr "Start" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menü" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "Ändere hinterlegte E-Mail-Adresse" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "Ändere Benutzernamen" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "Zwei-Faktor-Einrichtung" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "Single-User-Login-Einrichtung" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "Passkey-Einrichtung" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "Single-User-Login" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "Konto bestätigen" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "Ändere E-Mail-Adresse" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" "Nach dem Abschicken wird eine Bestätigungsmail an die neue E-Mail-Adresse" " gesendet." #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "Sie haben noch kein Passwort – hier können Sie eines setzen." #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "Aktueller Benutzername: %(username)s" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Anleitung zur Passwortzurücksetzung versenden" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "oder" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "Verwende einen Passkey zum Anmelden" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "Mit einem Passkey anmelden" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "Wiederherstellungscode eingeben" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "Wiederherstellungscodes" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" "Kopieren Sie diese unbedingt und bewahren Sie die Wiederherstellungscodes" " an einem sicheren Ort auf. Jeder Wiederherstellungscode kann nur einmal " "verwendet werden." #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "Erzeuge neue Wiederherstellungscodes" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "Wiederherstellung über Benutzernamen" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Passwort zurücksetzen" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Bestätigungslink erneut versenden" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "Zwei-Faktor-Methode auswählen" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" "Zwei-Faktor-Authentisierung bietet eine zusätzliche Sicherheitsebene für " "Ihr Benutzerkonto" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" "Sie müssen zusätzlich zu Benutzername und Passwort ein Einmalpasswort " "angeben." #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "Aktivierte Zwei-Faktor-Methode: %(method)s" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" "Öffnen Sie die Authentisierungs-App z.B. auf Ihrem Smartphone und scannen" " Sie den folgenden QR-Code (oder geben Sie den unten stehenden " "Einrichtungsscode ein), um mit der Erzeugung von Authentisierungscodes zu" " beginnen:" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "Zwei-Faktor-Authentisierungscode" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "Geben Sie den Bestätigungscode ein um die Einrichtung abzuschließen" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "geben Sie den numerischen Code ein" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "Diese Anwendung unterstützt Wiederherstellungscodes." #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "Sie können diese hier einrichten." #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "Passkeys" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "Diese Anwendung unterstützt Passkeys." #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "Zwei-Faktor-Authentisierung" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" "Bitte geben Sie den Authentisierungscode ein, den Sie via %(method)s " "erhalten haben." #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "Der Authentisierungscode wurde an Ihre E-Mail-Adresse gesendet" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "Eine Anfrage zum Rücksetzen des Accounts wurde per E-Mail an uns gesendet" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "Single-User-Login einrichten" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Passwortloser QR-Code" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "Keine Methode ausgewählt – keine Einrichtung erfolgt" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "Geben Sie das Einmalpasswort hier ein, um die Einrichtung abzuschließen" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "Senden eines Einmalpassworts anfordern" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "Neu anmelden" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Das Einmalpasswort wurde verschickt" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "Mit einem Passkey neu anmelden" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "Richte einen neuen Passkey ein" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "Geben Sie zunächst einen eindeutigen Bezeichner für den Passkey ein:" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "Bereits eingetragene Passkeys:" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" "Bezeichner: \"%s\" Verwendung: \"%s\" Transports: \"%s\" Entdeckbar: " "\"%s\" Gerätetyp: \"%s\" Gesichert? \"%s\" Zuletzt verwendet am: %s" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "Lösche einen eingetragenen Passkey" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "Mit einem Passkey anmelden" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "Mit einem Passkey anmelden" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "Einen Passkey als zweiten Faktor verwenden" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "Mit einem Passkey neu anmelden" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" "Verwenden Sie diesen Link um die neue E-Mail-" "Adresse zu bestätigen." #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "Dieser Link läuft in %(within)s ab." #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "Ihre derzeit hinterlegte E-Mail-Adresse lautet %(email)s." #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "Verwenden Sie %(link)s um Ihre neue E-Mail-Adresse zu bestätigen." #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Ihr Passwort wurde geändert." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Falls Sie Ihr Passwort nicht geändert haben," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "klicken Sie hier, um es zurückzusetzen" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" "Wenn Sie Ihr Passwort nicht geändert haben, klicken Sie bitte auf den " "unten stehenden Link, um es zurückzusetzen." #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "Ihr Benutzername wurde geändert." #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" "Verwende diesen Link um die E-Mail-" "Adresse zu bestätigen." #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "Verwende %(confirmation_link)s um die E-Mail-Adresse zu bestätigen." #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Willkommen %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Die Anmeldung kann über den Link unten erfolgen:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Jetzt anmelden" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "Willkommen %(username)s!" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" "Sie können sich mit dem folgenden Einmalpasswort an Ihrem Account " "anmelden: %(token)s" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "kann E-Mail-Konto nicht erreichen" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "Oder verwenden Sie folgenden Link: Anmelden" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "Oder verwenden Sie diesen Link: %(login_link)s" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "Hallo," #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "Sie haben kürzlich die Wiederherstellung Ihres Benutzernamens angefragt." #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "Ihr Benutzername ist: %(username)s" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" "Wenn Sie diese Anfrage nicht selbst gestellt haben, können Sie diese Mail" " ignorieren." #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "Hallo %(email)s!" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" "Jemand, eventuell Sie selbst, hat versucht sich mit dieser E-Mail-" "Adresse, die bereits in unserem System hinterlegt ist, zu registrieren." #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "Dieses Konto ist auch mit folgendem Benutzernamen verknüpft: %(username)s." #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" "Sie können diesen Link verwenden um Ihr " "Passwort zurückzusetzen." #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" "Sie haben Ihre E-Mail-Adresse noch nicht bestätigt - verwenden Sie dazu " "diesen Link." #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" "Mit diesem Konto ist auch der folgende Benutzername verknüpft: " "%(username)s" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" "Sie können diesen Link %(reset_link)s benutzen um Ihr Passwort " "zurückzusetzen." #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" "Sie haben Ihre E-Mail-Adresse noch nicht bestätigt - verwenden Sie dazu " "diesen Link: %(confirmation_link)s." #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" "Sie haben versucht sich mit dem Benutzernamen \"%(username)s\" zu " "registrieren. Dieser Benutzername ist aber bereits vergeben." #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" "Bitte starten Sie den Registrierungsprozess mit einem anderen " "Benutzernamen erneut." #~ msgid "You successfully confirmed password" #~ msgstr "" #~ msgid "Password confirmation is needed in order to access page" #~ msgstr "" #~ msgid "" #~ "Open your authenticator app on your " #~ "device and scan the following qrcode " #~ "to start receiving codes:" #~ msgstr "" #~ msgid "Or use the the link below:" #~ msgstr "Oder nutzen Sie den folgenden Link:" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ "Zusätzlich zu Ihrem Benutzernamen und " #~ "Passwort müssen Sie einen Code " #~ "verwenden, den wir Ihnen zusenden" #~ msgid "Please enter your authentication code" #~ msgstr "Bitte geben Sie den Authentisierungscode ein" #~ msgid "Setup Unified Sign In options" #~ msgstr "Single-User-Login einrichten" #~ msgid "Please re-authenticate" #~ msgstr "Bitte neu authentisieren, um auf diese Seite zuzugreifen." #~ msgid "Please Enter Your Password" #~ msgstr "Bitte geben Sie Ihr Passwort ein" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Für diesen Benutzer ist kein Passwort gesetzt" #~ msgid "Invalid Token" #~ msgstr "Ungültiger Token" #~ msgid "Your token has been confirmed" #~ msgstr "Ihr Token wurde bestätigt" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ "Öffnen Sie die Authentisierungs-App z.B." #~ " auf Ihrem Smartphone und scannen Sie" #~ " den folgenden QR-Code (oder geben" #~ " Sie den unten stehenden Code ein)," #~ " um den Empfang von Anmelde-Codes " #~ "zu beginnen" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ "Nickname: \"%s\" Verwendung: \"%s\" Transport:" #~ " \"%s\" Entdeckbar: \"%s\" Zuletzt " #~ "verwendet am: %s" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "Das Passwort wurde nicht innerhalb von" #~ " %(within)s zurückgesetzt. Eine neue " #~ "Anleitung wurde an %(email)s gesendet." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "Die E-Mail-Adresse wurden nicht " #~ "innerhalb von %(within)s bestätigt. Neue " #~ "Instruktionen wurden an %(email)s gesendet." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "" #~ "Sie sind nicht angemeldet. Bitte geben" #~ " Sie die korrekten Zugangsdaten ein." #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "Mögliche Anmeldemöglichkeiten:" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ "Um die Anmeldung abzuschließen, geben " #~ "Sie bitte den Code ein, den wir" #~ " an Ihre E-Mail-Adresse geschickt " #~ "haben" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "An welche Telefonnummer sollen wir den Code senden?" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ "Wir haben eine E-Mail von Ihnen " #~ "erhalten, um Ihnen bei der " #~ "Wiederherstellung Ihres Kontos zu unterstützen" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ "Es trat ein Fehler bei der " #~ "Kommunikation mit dem OAuth-Provider " #~ "auf. Bitte versuchen Sie es erneut." #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "" #~ "Vielen Dank. Um Ihre E-Mail-Adresse " #~ "%(email)s zu bestätigen, klicken Sie " #~ "bitte auf den Link in der E-Mail," #~ " die wir gerade an Sie gesendet " #~ "haben." #~ msgid "Two-factor Login" #~ msgstr "Zwei-Faktor-Anmeldung" #~ msgid "Two-factor Rescue" #~ msgstr "Zwei-Faktor-Wiederherstellung" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Bitte neu authentisieren, um auf diese Seite zuzugreifen." #~ msgid "You successfully disabled two factor authorization." #~ msgstr "Zwei-Faktor-Authentisierung wurde deaktiviert" #~ msgid "Disable two factor authentication" #~ msgstr "Deaktiviere Zwei-Faktor-Authentisierung" #~ msgid "Two Factor Setup" #~ msgstr "Zwei-Faktor-Einrichtung" #~ msgid "Sign in with " #~ msgstr "Anmelden mit " #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "Zwei-Faktor-Methode auswählen" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ "Zwei-Faktor-Authentisierung bietet eine " #~ "zusätzliche Sicherheitsebene für Ihr " #~ "Benutzerkonto" #~ msgid "Two factor authentication code" #~ msgstr "Zwei-Faktor-Authentisierungscode" #~ msgid "Two-factor Authentication" #~ msgstr "Zwei-Faktor-Authentisierung" #~ msgid "Please Reauthenticate" #~ msgstr "Bitte authentisieren Sie sich erneut" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "Bitte authentisieren Sie sich mit Ihrem WebAuthn-Sicherheitsschlüssel" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Passwort ändern" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Mein Konto bestätigen" #~ msgid "You can log into your account using the following code:" #~ msgstr "Sie können sich mit folgendem Code in Ihr Benutzerkonto anmelden:" #~ msgid "You can sign into your account using the following code:" #~ msgstr "Sie können sich mit folgendem Code in Ihr Benutzerkonto anmelden:" #~ msgid "Or use the link below:" #~ msgstr "Oder nutzen Sie den folgenden Link:" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Bitte die E-Mail-Adresse durch den Link unten bestätigen:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "" #~ "Bestätigen Sie Ihre E-Mail-Adresse, " #~ "indem Sie auf den Link unten " #~ "klicken:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "Wenn Sie Ihr Passwort vergessen habne, können Sie es" #~ msgid " here." #~ msgstr " hier zurücksetzen." #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ "Sollten Sie Ihr Passwort vergessen " #~ "haben, können Sie es unter folgendem " #~ "Link zurücksetzen:" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "Code zur Anmeldung: %(code)s" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ "Der WebAuthn-Vorgang muss innerhalb von" #~ " %(within)s beendet werden. Bitte erneut" #~ " versuchen." #~ msgid "Nickname for new credential is required." #~ msgstr "Zum Setzen eines neuen Webauthn-Tokens ist ein Nickname erforderlich." #~ msgid "%(name)s is already associated with a credential." #~ msgstr "%(name)s ist bereits mit einem Webauthn-Token verknüpft." #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "Der Token '%(name)s' wurde entfernt." #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "Der WebAuthn-Token '%(name)s' wurde hinzugefügt." #~ msgid "WebAuthn credential id already registered." #~ msgstr "WebAuthn-Token-ID wurde bereits eingetragen." #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "Nicht eingetragene WebAuthn-Token-ID." #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "WebAuthn-Token gehört zu keinem Benutzer." #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "Konnte WebAuthn-Token nicht verifizieren: %(cause)s." #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "WebAuthn-Token ist für diese Verwendung nicht eingetragen." #~ msgid "webauthn" #~ msgstr "WebAuthn" #~ msgid "WebAuthn Setup" #~ msgstr "WebAuthn-Einrichtung" #~ msgid "Forgot password" #~ msgstr "Passwort vergessen" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "Nutze WebAuthn zur Anmeldung" #~ msgid "Sign in with WebAuthn" #~ msgstr "Anmelden mit WebAuthn" #~ msgid "WebAuthn" #~ msgstr "WebAuthn" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "Diese Anwendung unterstützt WebAuthn-Sicherheitsschlüssel." #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ "Bitte nutzen Sie ein WebAuthn-" #~ "Sicherheitsschlüssel, um sich erneut zu " #~ "authentisieren" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "Neuen WebAuthn-Sicherheitsschlüssel einrichten" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ "Geben Sie zunächst einen eindeutigen " #~ "Namen für den neuen Sicherheitsschlüssel " #~ "ein:" #~ msgid "Currently registered security keys:" #~ msgstr "Bereits registrierte Sicherheitsschlüssel:" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "Lösche existierenden WebAuth-Sicherheitsschlüssel" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "Mit WebAuthn-Sicherheitsschlüssel anmelden" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "Verwenden Sie Ihren WebAuthn-Sicherheitsschlüssel als zweiten Faktor" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider: (%(exerror)s -" #~ " %(exdesc)s). Please try again." #~ msgstr "" #~ "Es trat ein Fehler bei der " #~ "Kommunikation mit dem OAuth-Provider " #~ "auf: (%(exerror)s - %(exdesc)s). Bitte " #~ "versuchen Sie es erneut." #~ msgid "Use Social Oauth to Sign In" #~ msgstr "Nutze Social OAuth zur Anmeldung" #~ msgid "Sign in with %(provider)s" #~ msgstr "Mit %(provider)s anmelden\"" #~ msgid "Username contains illegal characters" #~ msgstr "Der Benutzername enthält ungültige Zeichen" #~ msgid "Sign out" #~ msgstr "Abmelden" #~ msgid "Click here to reset your password" #~ msgstr "Klicken Sie hier, um das Passwort zurückzusetzen" #~ msgid "Click the link below to reset your password:" #~ msgstr "" #~ "Klicken Sie auf den unten stehenden " #~ "Link, um Ihr Passwort zurückzusetzen:" #~ msgid "You can sign in to your account using the following code: %(token)s" #~ msgstr "" #~ "Sie können sich mit folgendem " #~ "Einmalpasswort an Ihrem Account anmelden: " #~ "%(token)s" #~ msgid "You can sign in to your account using the following code: %(token)s." #~ msgstr "" #~ "Sie können sich mit folgendem " #~ "Einmalpasswort an Ihrem Account anmelden: " #~ "%(token)s" pallets-eco-flask-security-72bf929/flask_security/translations/es_ES/000077500000000000000000000000001525721743500260145ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/es_ES/LC_MESSAGES/000077500000000000000000000000001525721743500276015ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/es_ES/LC_MESSAGES/flask_security.po000066400000000000000000001320431525721743500331730ustar00rootroot00000000000000# Spanish (Spain) translations for Flask-Security. # Copyright (C) 2026 THE AUTHORS # This file is distributed under the same license as the Flask-Security # project. # Mauko Quiroga , 2017. # Martin Mozos , 2020. # Giorgio Stampa <22690890+gissimo@users.noreply.github.com>, 2026. # Ariel von Barnekow , 2025. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 5.9.0\n" "Report-Msgid-Bugs-To: jwag956@github.com\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: 2026-08-29 14:14+0200\n" "Last-Translator: Ariel von Barnekow \n" "Language: es_ES\n" "Language-Team: \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "Confirma tu nueva dirección de correo electrónico" #: flask_security/core.py:363 msgid "Login Required" msgstr "Inicio de sesión necesario" #: flask_security/core.py:364 msgid "Welcome" msgstr "Bienvenido/a" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "Por favor, confirma tu correo electrónico" #: flask_security/core.py:366 msgid "Login instructions" msgstr "Instrucciones para iniciar sesión" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "Tu contraseña ha sido restablecida" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "Tu contraseña ha sido cambiada" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "Instrucciones de recuperación de contraseña" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "Tu nombre de usuario ha sido cambiado" #: flask_security/core.py:371 msgid "Your requested username" msgstr "Tu nombre de usuario solicitado" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "Inicio de sesión de doble factor" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "Recuperación del factor de doble autenticación" #: flask_security/core.py:423 msgid "Verification Code" msgstr "Código de verificación" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "Entrada no apropiada para la API solicitada" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" "Fallo de autenticación - identidad o contraseña/código de acceso no " "válidos" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" "Si esa dirección de correo electrónico está en nuestro sistema, recibirás" " un correo electrónico con la descripción de como restablecer tu " "contraseña." #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "Si esa identidad está en nuestro sistema, se te envió un código." #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "No tienes permiso para consultar este recurso." #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "Debes autenticarte para acceder a este recurso." #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "Debes volver a autenticarte para acceder a este recurso" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" "Gracias. Para confirmar tu dirección de correo electrónico %(email)s, haz" " clic en el enlace del mensaje que acabamos de enviarte." #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "Gracias. Tu correo electrónico ha sido confirmado." #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "Tu correo electrónico ya ha sido confirmado." #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "Código de confirmación inválido." #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s ya está asociado a una cuenta." #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" "El atributo de identidad '%(attr)s' con el valor '%(value)s' ya está " "asociado con una cuenta." #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "Identidad %(id)s no registrada" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" "Ocurrió un error al comunicarse con el proveedor de OAuth: (%(exerror)s -" " %(exdesc)s). Por favor, inténtalo de nuevo." #: flask_security/core.py:531 msgid "Password does not match" msgstr "La contraseña no coincide" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "Las contraseñas no coinciden" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "Las redirecciones a sitios web externos están prohibidas" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "Código de recuperación inválido" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "Aún no se han generado códigos de recuperación" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "" "Las instrucciones para restablecer tu contraseña se han enviado a " "%(email)s." #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "No has restablecido tu contraseña en un plazo de %(within)s. " #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "Código de restablecimiento de contraseña inválido." #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "El correo electrónico requiere confirmación." #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Las instrucciones de confirmación se han enviado a %(email)s." #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "No has confirmado tu correo electrónico en un plazo de %(within)s. " #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "No has iniciado sesión antes de %(within)s. Nuevas instrucciones para " "iniciar sesión se han enviado a %(email)s." #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Las instrucciones para iniciar sesión se han enviado a %(email)s." #: flask_security/core.py:565 msgid "Invalid login token." msgstr "Código de inicio de sesión inválido." #: flask_security/core.py:566 msgid "Account is disabled." msgstr "Cuenta deshabilitada." #: flask_security/core.py:567 msgid "Email not provided" msgstr "Correo electrónico no indicado" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "Dirección de correo electrónico inválida" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "Código inválido" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "El valor ingresado no es válido para el campo" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "El valor ingresado debe ser menor que %(length) caracteres" #: flask_security/core.py:578 msgid "Password not provided" msgstr "Contraseña no proporcionada" #: flask_security/core.py:580 #, python-format msgid "Password must be at least %(length)s characters" msgstr "La contraseña debe tener al menos %(length)s caracteres" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "La contraseña no es lo suficientemente compleja" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "La contraseña aparece en la lista de contraseñas filtradas" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "No se ha podido contactar con el sitio de contraseñas filtradas" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "El número de teléfono no es válido, p. ej. falta el código de país" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "El usuario especificado no existe" #: flask_security/core.py:591 msgid "Invalid password" msgstr "Contraseña inválida" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "La contraseña o el código facilitado son inválidos" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "Has iniciado sesión correctamente." #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "¿Has olvidado tu contraseña?" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "Has restablecido tu contraseña y has iniciado sesión automáticamente." #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "Has restablecido tu contraseña. Inicia sesión con tu nueva contraseña." #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "Tu nueva contraseña debe ser diferente de la antigua." #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "Has cambiado tu contraseña." #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "Debes iniciar sesión para poder acceder a esta página." #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "Debes volver a autenticarte para poder acceder a esta página." #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "El token de actualización no es válido (%(reason)s)" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "Reautenticación exitosa" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "Solo puedes acceder a este recurso si no has iniciado sesión." #: flask_security/core.py:622 msgid "Code has been sent." msgstr "El código se ha enviado." #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "No se pudo enviar el código. Por favor inténtalo de nuevo más tarde" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "Tu código ha sido confirmado" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "Has cambiado tu método de doble autenticación." #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "Actualmente no tienes permisos para acceder a esta página" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "El método marcado no es válido" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "Has deshabilitado la autenticación de doble factor." #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" "La configuración debe completarse en el plazo de %(within)s. Por favor " "vuelve a empezar." #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "Opciones de inicio de sesión activas actualmente: %(method_list)s." #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "El método solicitado no es válido" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "El inicio de sesión unificado se ha configurado correctamente" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "Debes especificar una identidad válida para iniciar sesión" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "Utiliza este código para iniciar sesión: %(code)s" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "Has cambiado tu nombre de usuario" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" "El nombre de usuario debe tener al menos %(min)d caracteres y menos de " "%(max)d caracteres" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "El nombre de usuario solo puede contener letras y números" #: flask_security/core.py:667 msgid "Username not provided" msgstr "Nombre de usuario no proporcionado" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "%(username)s ya está asociado a una cuenta." #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" "Las operaciones con llaves de acceso deben completarse en el plazo de " "%(within)s. Por favor vuelve a empezar." #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "Se requiere un nombre para la nueva llave de acceso." #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "%(name)s ya está asociado a una llave de acceso." #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "%(name)s no registrado con el usuario actual." #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" "Los apodos de las llaves de acceso pueden contener solo letras, números y" " puntuación limitada." #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "Se ha eliminado la llave de acceso con nombre: %(name)s" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "Se ha añadido la llave de acceso con nombre: %(name)s" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "Llave de acceso ya registrada." #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "Llave de acceso no registrada." #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "La llave de acceso no pertenece a ningún usuario." #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "No se pudo verificar la llave de acceso: %(cause)s." #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "Llave de acceso no registrada para este uso (primario o secundario)" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "La credencial de usuario no coincide" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" "La confirmación debe completarse en el plazo de %(within)s. Por favor " "vuelve a empezar." #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "Cambio de dirección de correo electrónico confirmado" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" "Las instrucciones para confirmar tu dirección de correo electrónico se " "han enviado a %(email)s." #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" "Si estás registrado, tu nombre de usuario se enviará a tu correo " "electrónico." #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" "Configurar con una aplicación de autenticación (p.ej. Google, LastPass, " "Authy)" #: flask_security/forms.py:72 msgid "Change Method" msgstr "Cambiar el método" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Cambiar la contraseña" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "Código de autenticación" #: flask_security/forms.py:75 msgid "Delete" msgstr "Eliminar" #: flask_security/forms.py:76 msgid "Email Address" msgstr "Correo electrónico" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "Configurar con correo electrónico" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "Error(es)" #: flask_security/forms.py:79 msgid "Identity" msgstr "Identidad" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Iniciar sesión" #: flask_security/forms.py:81 msgid "New Password" msgstr "Nueva contraseña" #: flask_security/forms.py:82 msgid "Passcode" msgstr "Código de acceso" #: flask_security/forms.py:83 msgid "Password" msgstr "Contraseña" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "Número de teléfono" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "Recuperar contraseña" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "Recuperar nombre de usuario" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Registrarse" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "Recuérdame" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "Restablecer contraseña" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "Confirmar contraseña" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "Reenviar las instrucciones de confirmación" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "Enviar enlace para iniciar sesión" #: flask_security/forms.py:93 msgid "Send Code" msgstr "Enviar código" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Iniciar sesión" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "Configurar con SMS" #: flask_security/forms.py:96 msgid "Submit" msgstr "Enviar" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "Confirmar código" #: flask_security/forms.py:98 msgid "Username" msgstr "Nombre de usuario" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "Verificar contraseña" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "Google Authenticator" #: flask_security/forms.py:105 msgid "authenticator" msgstr "aplicación de autenticación" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "correo electrónico" #: flask_security/forms.py:108 msgid "SMS" msgstr "SMS" #: flask_security/forms.py:109 msgid "password" msgstr "contraseña" #: flask_security/forms.py:110 msgid "passkey" msgstr "llave de acceso" #: flask_security/forms.py:111 msgid "none" msgstr "ninguno" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "Token de actualización" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "Métodos disponibles" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "Deshabilitar la autenticación de doble factor" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "¿Problemas para acceder a tu cuenta/has perdido tu dispositivo móvil?" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "Contactar con el administrador" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "Mostrar los códigos de recuperación" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "Generar nuevos códigos de recuperación" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "Código de recuperación" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "Métodos de doble factor disponibles:" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "Seleccionar" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "Enviar el código por correo electrónico" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "Usar código de recuperación descargado anteriormente" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "Código o contraseña" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "Con correo electrónico" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "Con SMS" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "Configurar una opción de inicio de sesión adicional" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "Eliminar la opción de inicio de sesión activa" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "Apodo" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "Uso" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "Uso como factor de autenticación primario" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "Uso como factor de autenticación secundario" #: flask_security/webauthn.py:241 msgid "Start" msgstr "Iniciar" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menú" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "Cambiar el correo electrónico registrado" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "Cambiar el nombre de usuario" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "Configuración de doble factor de autenticación" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "Configuración de inicio de sesión unificado" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "Configuración de llave de acceso" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "Inicio de sesión unificado" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "Confirmar cuenta" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "Cambiar el correo electrónico" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" "Un mensaje de confirmación se enviará a esta nueva dirección de correo " "electrónico." #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "Actualmente no tienes una contraseña - esto añadirá una." #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "El nombre de usuario actual es: %(username)s" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Enviar instrucciones para restablecer la contraseña" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "o" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "Usar una llave de acceso para iniciar sesión" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "Iniciar sesión con llave de acceso" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "Usar el inicio de sesión social para acceder" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "Continuar con %(provider)s" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "Confirmar cierre de sesión" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "Introducir código de recuperación" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "Códigos de recuperación" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" "Asegúrate de copiarlos y guardarlos en un lugar seguro. Cada código solo " "puede utilizarse una vez." #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "Generar nuevos códigos de recuperación" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "Recuperación del nombre de usuario" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Restablecer contraseña" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Reenviar instrucciones de confirmación" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "Seleccionar método de doble factor" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" "La autenticación de doble factor añade una capa adicional de seguridad a " "tu cuenta" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "Además de tu nombre de usuario y contraseña, deberás utilizar un código." #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "Método de dos factores actualmente configurado: %(method)s" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" "Abre una aplicación de autenticación en tu dispositivo y escanea el " "siguiente código QR (o ingresa manualmente el código que aparece a " "continuación) para comenzar a recibir códigos:" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "Código de autenticación de doble factor" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "Introduce el código para completar la configuración" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "introduce el código numérico" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "Esta aplicación permite establecer códigos de recuperación." #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "Los puedes configurar aquí." #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "Llaves de acceso" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "Esta aplicación es compatible con llaves de acceso." #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "Autenticación de doble factor" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "Introduce tu código de autenticación generado a través de: %(method)s" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "El código de autenticación se envió a tu dirección de correo electrónico" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" "Se nos envió un correo electrónico para restablecer tu cuenta de " "aplicación" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "Configurar el inicio de sesión unificado" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Código QR sin contraseña" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "No se ha habilitado ningún método, no hay nada que configurar" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "Introduce el código aquí para completar la configuración" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "Solicitar que se envíe un código de un solo uso" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "Vuelve a autenticarte" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Se envió el código" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "Utilizar una llave de acceso para volver a autenticarte" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "Usar el inicio de sesión social para volver a autenticarte" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "Configurar una nueva llave de acceso" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "Comienza por proporcionar un nombre único para tu nueva llave de acceso:" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "Llaves de acceso actualmente registradas:" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" "Nombre: \"%s\" Uso: \"%s\" Transportes: \"%s\" Descubrible: \"%s\" Tipo " "de dispositivo: \"%s\" ¿Con copia de respaldo? \"%s\" Utilizado por " "última vez: \"%s" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "Eliminar una llave de acceso existente" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "Iniciar sesión con una llave de acceso" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "Iniciar sesión con una llave de acceso" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "Utilizar una llave de acceso como segundo factor" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "Vuelve a autenticarte con una llave de acceso" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" "Usa este enlace para confirmar tu dirección de " "correo electrónico." #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "Este enlace caducará en %(within)s." #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "Tu correo electrónico registrado actualmente es %(email)s." #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "Usa %(link)s para confirmar tu dirección de correo electrónico." #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Tu contraseña ha sido cambiada." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Si no has cambiado tú la contraseña," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "haz clic aquí para restablecerla" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" "Si no has cambiado tú la contraseña, haz clic en el enlace de abajo para " "restablecerla." #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "Tu nombre de usuario ha sido cambiado." #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" "Usa este enlace para confirmar tu " "dirección de correo electrónico." #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "" "Usa %(confirmation_link)s para confirmar tu dirección de correo " "electrónico." #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "¡Bienvenido/a %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Inicia sesión a través del enlace de abajo:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Inicia sesión ahora" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" "Utiliza este enlace para restablecer tu " "contraseña." #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "Utiliza %(reset_link)s para restablecer tu contraseña." #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "Tu contraseña ha sido restablecida." #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "¡Bienvenido/a %(username)s!" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" "Puedes iniciar sesión en tu cuenta utilizando el siguiente código: " "%(token)s" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "Este código expirará en %(within)s." #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "no puede acceder a la cuenta de correo" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" "Puedes iniciar sesión en tu cuenta utilizando el siguiente código: " "%(code)s" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "O puedes utilizar este enlace: Autenticarse" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "Este código/enlace expirará en %(within)s." #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "O puedes utilizar este enlace: %(login_link)s" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "Hola," #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "Recientemente has solicitado recuperar tu nombre de usuario." #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "Tu nombre de usuario es: %(username)s" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "Si no has iniciado esta solicitud, puedes ignorar este mensaje." #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "¡Hola %(email)s!" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" "Alguien (¿tú?) ha intentado registrar este correo electrónico, que ya " "está en nuestro sistema." #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" "Esta cuenta también tiene asociado el siguiente nombre de usuario: " "%(username)s." #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" "Puedes usar este enlace para restablecer " "tu contraseña." #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" "Aún no has confirmado tu dirección de correo electrónico - utiliza este enlace para hacerlo ahora." #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" "Esta cuenta también tiene asociado el siguiente nombre de usuario: " "%(username)s" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "Puedes usar este enlace %(reset_link)s para restablecer tu contraseña." #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" "Aún no has confirmado tu dirección de correo electrónico - utiliza este " "enlace: %(confirmation_link)s para hacerlo ahora." #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" "Has intentado registrarte con un nombre de usuario \"%(username)s\" que " "ya está asociado a otra cuenta." #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" "Por favor reinicia el proceso de registro con un nombre de usuario " "diferente." #~ msgid "Username contains illegal characters" #~ msgstr "El nombre de usuario contiene caracteres no admitidos" #~ msgid "Sign out" #~ msgstr "Cerrar sesión" #~ msgid "Click here to reset your password" #~ msgstr "Haz clic aquí para restablecer la contraseña" #~ msgid "Click the link below to reset your password:" #~ msgstr "Haz clic en el enlace de abajo para restablecer la contraseña:" #~ msgid "You can sign in to your account using the following code: %(token)s" #~ msgstr "" #~ "Puedes iniciar sesión en tu cuenta " #~ "utilizando el siguiente código: %(token)s" #~ msgid "You can sign in to your account using the following code: %(token)s." #~ msgstr "" #~ "Puedes iniciar sesión en tu cuenta " #~ "utilizando el siguiente código: %(token)s." pallets-eco-flask-security-72bf929/flask_security/translations/eu_ES/000077500000000000000000000000001525721743500260165ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/eu_ES/LC_MESSAGES/000077500000000000000000000000001525721743500276035ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/eu_ES/LC_MESSAGES/flask_security.po000066400000000000000000001277721525721743500332120ustar00rootroot00000000000000# Basque (Spain) translations for Flask-Security. # Copyright (C) 2020 # This file is distributed under the same license as the Flask-Security # project. # Martin Mozos , 2020. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 4.0.0\n" "Report-Msgid-Bugs-To: jwag956@github.com\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: 2020-11-28 13:41+0100\n" "Last-Translator: Martin Mozos \n" "Language: eu_ES\n" "Language-Team: \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:363 msgid "Login Required" msgstr "Saioa hasi behar da" #: flask_security/core.py:364 msgid "Welcome" msgstr "Ongi etorri" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "Mesedez berretsi zure posta elektronikoa" #: flask_security/core.py:366 msgid "Login instructions" msgstr "Saioa hasteko argibideak" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "Zure pasahitza berrezarri da" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "Zure pasahitza aldatu da" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "Pasahitza berreskuratzeko argibideak" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:371 msgid "Your requested username" msgstr "" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:423 msgid "Verification Code" msgstr "Egiaztapen kodea" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "Sarrera ez da egokia eskatutako APIarentzat" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "Ez duzu baliabide hau kontsultatzeko baimenik." #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "Eskerrik asko. Zure posta elektronikoa berretsi da." #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "Zure posta elektronikoa dagoeneko baieztatuta dago." #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "Berrespen token baliogabea." #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s dagoeneko kontu batekin lotuta dago." #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" "'%(attr)s' identitate atributua '%(value)s' balioarekin dagoeneko kontu " "batekin lotuta dago" #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:531 msgid "Password does not match" msgstr "Pasahitza ez dator bat" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "Pasahitzak ez datoz bat" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "Domeinutik kanpoko birbideratzeak debekatuta daude" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "Pasahitza berrezartzeko argibideak %(email)s helbidera bidali dira." #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "Pasahitza berrezartzeko token baliogabea." #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "Mezu elektronikoak berrespena behar du." #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Baieztapen argibideak %(email)s helbidera bidali dira." #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Ez duzu saioa hasi %(within)s barruan. Saioa hasteko argibide berriak " "%(email)s helbidera bidali dira." #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Saioa hasteko argibideak %(email)s helbidera bidali dira." #: flask_security/core.py:565 msgid "Invalid login token." msgstr "Saioa hasteko token baliogabea." #: flask_security/core.py:566 msgid "Account is disabled." msgstr "Kontua desgaituta dago." #: flask_security/core.py:567 msgid "Email not provided" msgstr "Helbide elektronikoa beharrezkoa da" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "Helbide elektroniko baliogabea" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "Kode baliogabea" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "" #: flask_security/core.py:578 msgid "Password not provided" msgstr "Pasahitza beharrezkoa da" #: flask_security/core.py:580 #, python-format msgid "Password must be at least %(length)s characters" msgstr "Pasahitzak gutxienez %(length)s karaktere izan behar ditu" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "Pasahitza ez da nahikoa konplexua" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "Pasahitza urratutako zerrendan" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "Ezin izan da urratutako pasahitzen iturburuarekin harremanetan jarri" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "Telefono zenbakiak ez du balio, baliteke herrialde kodea faltatzea" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "Zehaztutako erabiltzea ez da existitzen" #: flask_security/core.py:591 msgid "Invalid password" msgstr "Pasahitz okerra" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "Zehaztutako pasahitzak edo kodeak ez du balio" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "Behar bezala hasi duzu saioa." #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "Pasahitza ahaztua?" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "Pasahitza berrezarri duzunez automatikoki hasi duzu saioa." #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "Zure pasahitz berriak zure aurreko pasahitzaren ezberdin behar du izan." #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "Pasahitza behar bezala aldatu duzu." #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "Hasi saioa orri honetara sartzeko." #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "Mesedez, berriro autentifikatu orri honetara sartzeko." #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "Berautentifikatzea osatu da" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "Saioa amaitzen ez duzunean soilik sar zaitezke baliabide honetara" #: flask_security/core.py:622 msgid "Code has been sent." msgstr "" #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "Ezin izan da kodea bidali. Saiatu berriro geroago" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "Bi faktoretako metodoa ondo aldatu duzu." #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "Une honetan ez duzu baimenik orrialde honetara sartzeko" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "Markatutako metodoak ez du balio" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "Konfigurazioa %(within)s barruan osatu behar da. Mesedez, berriro hasi." #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "Eskatutako metodoak ez du balio" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "Bateratutako saio hasierarako konfigurazioa ongi egin da" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "Saioa hasteko identitate baliagarria zehaztu behar duzu" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" "Erabiltzaile izenak gutxienez %(min)d karaktere eta %(max)d karaktere " "baino gutxiago izan behar ditu" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "Erabiltzaile izenak letrak eta zenbakiak soilik izan ditzake" #: flask_security/core.py:667 msgid "Username not provided" msgstr "Erabiltzaile izena ez da eman" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "%(username)s dagoeneko kontu batekin lotuta dago." #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" "Konfiguratu autentifikatzaile aplikazio bat erabiliz (google, lastpass " "edo authy adibidez)" #: flask_security/forms.py:72 msgid "Change Method" msgstr "Aldatzeko metodoa" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Aldatu pasahitza" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "Autentifikazio kodea" #: flask_security/forms.py:75 msgid "Delete" msgstr "" #: flask_security/forms.py:76 msgid "Email Address" msgstr "Posta elektronikoa" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "Konfiguratu posta elektronikoa erabiliz" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "Errorea(k)" #: flask_security/forms.py:79 msgid "Identity" msgstr "Identitatea" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Hasi saioa" #: flask_security/forms.py:81 msgid "New Password" msgstr "Pasahitz berria" #: flask_security/forms.py:82 msgid "Passcode" msgstr "Pasakodea" #: flask_security/forms.py:83 msgid "Password" msgstr "Pasahitza" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "Telefono zenbakia" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "Pasahitza berreskuratu" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Izena eman" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "Gogorarazi" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "Pasahitza berrezarri" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "Idatzi berriro pasahitza" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "Bidali berrespen argibideak" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "Bidali saioa hasteko esteka" #: flask_security/forms.py:93 msgid "Send Code" msgstr "Bidali kodea" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Hasi saioa" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "Konfiguratu SMS bidez" #: flask_security/forms.py:96 msgid "Submit" msgstr "Bidali" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "Bidali kodea" #: flask_security/forms.py:98 msgid "Username" msgstr "Erabiltzaile izena" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "Pasahitza ziurtatu" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:105 msgid "authenticator" msgstr "" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "" #: flask_security/forms.py:108 msgid "SMS" msgstr "" #: flask_security/forms.py:109 msgid "password" msgstr "" #: flask_security/forms.py:110 msgid "passkey" msgstr "" #: flask_security/forms.py:111 msgid "none" msgstr "" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "Eskuragarri dauden metodoak" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "Kodea edo pasahitza" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "Posta elektronikoaren bidez" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "SMS bidez" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:241 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menua" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "Saio hasiera bateratua" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "Berretsi kontua" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Bidali pasahitza berrezartzeko argibideak" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Pasahitza berrezarri" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Bidali berrespen argibideak" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" "Ireki autentifikazio aplikazio bat zure gailuan eta eskaneatu QR kode hau" " (edo idatzi beheko kodea eskuz) kodeak jasotzen hasteko:" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "Autentifikaziorako kodea zure helbide elektronikora bidali da" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Pasahitzik gabeko QR kodea" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "Ez da metodorik gaitu, ez dago ezer konfiguratzeko" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "Eskatu erabilera-bakarreko kodea bidaltzeko" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Kodea bidali da" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Zure pasahitza aldatu da." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Pasahitza aldatu ez baduzu," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "egin klik hemen berrezartzeko" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "Pasahitza aldatu ez baduzu, egin klik beheko estekan berrezartzeko." #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "" #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Ongi etorri %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Beheko estekaren bidez saioa has zenezake:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Hasi saioa orain" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "ezin da posta kontura sartu" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" #~ msgid "Username not allowed" #~ msgstr "Erabiltzaile izena ez da onartzen" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ "Zure erabiltzaile izenaz eta pasahitzaz " #~ "gain, bidaliko dizugun kodea erabili " #~ "beharko duzu" #~ msgid "Please enter your authentication code" #~ msgstr "Mesedez, sartu autentifikazio kodea" #~ msgid "Setup Unified Sign In options" #~ msgstr "Konfiguratu saioa hasteko aukera bateratuak" #~ msgid "Please re-authenticate" #~ msgstr "Mesedez, berriro autentifikatu" #~ msgid "Please Enter Your Password" #~ msgstr "Mesedez, sartu zure pasahitza" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Ez da pasahitzik ezarri erabiltzaile honentzat" #~ msgid "Invalid Token" #~ msgstr "Token baliogabea" #~ msgid "Your token has been confirmed" #~ msgstr "Zure token baieztatu da" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ "Ireki autentifikazio aplikazio bat zure " #~ "gailuan eta eskaneatu QR kode hau " #~ "(edo idatzi beheko kodea eskuz) " #~ "pasakodeak jasotzen hasteko:" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "Ez duzu zure pasahitza berrezarri " #~ "%(within)s barruan. Argibide berriak bidali" #~ " dira %(email)s-era." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "Ez duzu zure posta elektronikoa berretsi" #~ " %(within)s barruan. Zure posta " #~ "elektronikoa berresteko argibide berriak " #~ "%(email)s helbidera bidali dira." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "Ez zaude autentifikatuta. Mesedez, eman egiaztagiri zuzenak." #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ "Saioa hasten bukatzeko, idatzi zure " #~ "posta elektronikora bidalitako kodea" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "Zein telefono zenbakiri bidali beharko genioke kodea?" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "Zure eskaera kontua berrezartzeko mezu elektroniko bat bidali ziguten" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "Eskerrik asko. Baieztapen argibideak %(email)s helbidera bidali dira." #~ msgid "Two-factor Login" #~ msgstr "Bi faktoreko saioa hastea" #~ msgid "Two-factor Rescue" #~ msgstr "Bi faktoreko saioa berreskuratzea" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Baliabide honetara sartzeko berriro autentifikatu behar duzu" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "Bi faktoreren baimena behar bezala desgaitu duzu." #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ "Bi faktoreko autentifikazioak segurtasun " #~ "geruza gehigarri bat esleitzen dio zure" #~ " kontuari" #~ msgid "Two factor authentication code" #~ msgstr "Bi faktoretako autentifikazio kodea" #~ msgid "Two-factor Authentication" #~ msgstr "Bi faktoretako autentifikazioa" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Aldatu pasahitza" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Berretsi nire kontua" #~ msgid "You can log into your account using the following code:" #~ msgstr "Zure kontuan saioa has dezakezu kode hau erabiliz:" #~ msgid "You can sign into your account using the following code:" #~ msgstr "Zure kontuan saioa has dezakezu kode hau erabiliz:" #~ msgid "Or use the link below:" #~ msgstr "Edo erabili beheko esteka:" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Mesedez, berretsi zure posta elektronikoa beheko estekaren bidez:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Zure posta elektronikoa beheko estekaren bidez baiezta dezakezu:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "Erabili kode hau saioa hasteko: %(code)s." #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "Pasahitza ahaztua" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider: (%(exerror)s -" #~ " %(exdesc)s). Please try again." #~ msgstr "" #~ msgid "Use Social Oauth to Sign In" #~ msgstr "" #~ msgid "Sign in with %(provider)s" #~ msgstr "" #~ msgid "Username contains illegal characters" #~ msgstr "Erabiltzaile izenak legez kanpoko karaktereak ditu" #~ msgid "Sign out" #~ msgstr "" #~ msgid "Click here to reset your password" #~ msgstr "Egin klik hemen zure pasahitza berrezartzeko" #~ msgid "Click the link below to reset your password:" #~ msgstr "Egin klik beheko estekan zure pasahitza berrezartzeko:" #~ msgid "You can sign in to your account using the following code: %(token)s" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s." #~ msgstr "" pallets-eco-flask-security-72bf929/flask_security/translations/flask_security.pot000066400000000000000000000777731525721743500306160ustar00rootroot00000000000000# Translations template for Flask-Security. # Copyright (C) 2026 ORGANIZATION # This file is distributed under the same license as the Flask-Security # project. # FIRST AUTHOR , 2026. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: Flask-Security 5.9.0\n" "Report-Msgid-Bugs-To: jwag956@github.com\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:363 msgid "Login Required" msgstr "" #: flask_security/core.py:364 msgid "Welcome" msgstr "" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "" #: flask_security/core.py:366 msgid "Login instructions" msgstr "" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:371 msgid "Your requested username" msgstr "" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:423 msgid "Verification Code" msgstr "" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "" #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "" #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "" #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "" #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "" #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:531 msgid "Password does not match" msgstr "" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "" #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "" #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "" #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "" #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "" #: flask_security/core.py:565 msgid "Invalid login token." msgstr "" #: flask_security/core.py:566 msgid "Account is disabled." msgstr "" #: flask_security/core.py:567 msgid "Email not provided" msgstr "" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "" #: flask_security/core.py:578 msgid "Password not provided" msgstr "" #: flask_security/core.py:580 #, python-format msgid "Password must be at least %(length)s characters" msgstr "" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "" #: flask_security/core.py:591 msgid "Invalid password" msgstr "" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "" #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "" #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "" #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "" #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "" #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "" #: flask_security/core.py:622 msgid "Code has been sent." msgstr "" #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "" #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:667 msgid "Username not provided" msgstr "" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" #: flask_security/forms.py:72 msgid "Change Method" msgstr "" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "" #: flask_security/forms.py:75 msgid "Delete" msgstr "" #: flask_security/forms.py:76 msgid "Email Address" msgstr "" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "" #: flask_security/forms.py:79 msgid "Identity" msgstr "" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "" #: flask_security/forms.py:81 msgid "New Password" msgstr "" #: flask_security/forms.py:82 msgid "Passcode" msgstr "" #: flask_security/forms.py:83 msgid "Password" msgstr "" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "" #: flask_security/forms.py:93 msgid "Send Code" msgstr "" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "" #: flask_security/forms.py:96 msgid "Submit" msgstr "" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "" #: flask_security/forms.py:98 msgid "Username" msgstr "" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:105 msgid "authenticator" msgstr "" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "" #: flask_security/forms.py:108 msgid "SMS" msgstr "" #: flask_security/forms.py:109 msgid "password" msgstr "" #: flask_security/forms.py:110 msgid "passkey" msgstr "" #: flask_security/forms.py:111 msgid "none" msgstr "" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:241 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "" #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "" #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "" #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" pallets-eco-flask-security-72bf929/flask_security/translations/fr_FR/000077500000000000000000000000001525721743500260145ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/fr_FR/LC_MESSAGES/000077500000000000000000000000001525721743500276015ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/fr_FR/LC_MESSAGES/flask_security.po000066400000000000000000001310251525721743500331720ustar00rootroot00000000000000# French (France) translations for Flask-Security. # Copyright (C) 2026 THE AUTHORS # This file is distributed under the same license as the Flask-Security # project. # Alexandre Bulté , 2017. # Giorgio Stampa <22690890+gissimo@users.noreply.github.com>, 2026. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 5.9.0\n" "Report-Msgid-Bugs-To: jwag956@github.com\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: 2026-08-29 15:34+0200\n" "Last-Translator: Giorgio Stampa " "<22690890+gissimo@users.noreply.github.com>\n" "Language: fr_FR\n" "Language-Team: \n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "Confirmez votre nouvelle adresse email" #: flask_security/core.py:363 msgid "Login Required" msgstr "Connexion requise" #: flask_security/core.py:364 msgid "Welcome" msgstr "Bienvenue" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "Veuillez confirmer votre adresse email" #: flask_security/core.py:366 msgid "Login instructions" msgstr "Instructions de connexion" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "Votre mot de passe a été réinitialisé" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "Votre mot de passe a été changé" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "Instructions de réinitialisation de votre mot de passe" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "Votre nom d'utilisateur a été changé" #: flask_security/core.py:371 msgid "Your requested username" msgstr "Votre nom d'utilisateur demandé" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "Connexion à deux facteurs" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "Récupération de l'authentification à deux facteurs" #: flask_security/core.py:423 msgid "Verification Code" msgstr "Code de vérification" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "Entrée non appropriée pour l'API demandée" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" "Échec de l'authentification - identité ou mot de passe/code d'accès non " "valides" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" "Si cette adresse email figure dans notre système, vous recevrez un email " "vous expliquant comment réinitialiser votre mot de passe." #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "Si cette identité figure dans notre système, un code vous a été envoyé." #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "Vous n'avez pas l'autorisation d'accéder à cette ressource." #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "Vous devez vous connecter pour accéder à cette ressource." #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "Veuillez vous reconnecter pour accéder à cette ressource." #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" "Merci. Pour confirmer votre adresse email %(email)s, veuillez cliquer sur" " le lien contenu dans l'email que nous venons de vous envoyer." #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "Merci. Votre adresse email a été confirmée." #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "Votre adresse email a déjà été confirmée." #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "Token de confirmation non valide." #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "L'adresse %(email)s est déjà utilisée." #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" "L'attribut d'identité '%(attr)s' avec la valeur '%(value)s' est déjà " "associé à un compte." #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "Identité %(id)s non enregistrée" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" "Une erreur s'est produite lors de la communication avec le fournisseur " "OAuth : (%(exerror)s - %(exdesc)s). Veuillez réessayer." #: flask_security/core.py:531 msgid "Password does not match" msgstr "Le mot de passe ne correspond pas" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "Les mots de passe ne correspondent pas" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "Les redirections en dehors du domaine sont interdites" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "Code de récupération invalide" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "Aucun code de récupération n'a encore été généré" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "" "Les instructions de réinitialisation de votre mot de passe ont été " "envoyées à %(email)s." #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" "Vous n'avez pas réinitialisé votre mot de passe dans le délai de " "%(within)s. " #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "Token de réinitialisation non valide." #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "Une confirmation de l'adresse email est requise." #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Les instructions de confirmation ont été envoyées à %(email)s." #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "Vous n'avez pas confirmé votre adresse email dans le délai de %(within)s. " #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Vous ne vous êtes pas connecté(e) dans le délai de %(within)s. De " "nouvelles instructions de connexion ont été envoyées à %(email)s." #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Les instructions de connexion ont été envoyées à %(email)s." #: flask_security/core.py:565 msgid "Invalid login token." msgstr "Token de connexion non valide." #: flask_security/core.py:566 msgid "Account is disabled." msgstr "Le compte est désactivé." #: flask_security/core.py:567 msgid "Email not provided" msgstr "Merci d'indiquer une adresse email" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "Adresse email non valide" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "Code invalide" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "La valeur saisie n'est pas valide pour ce champ" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "La valeur saisie doit contenir moins de %(length) caractères" #: flask_security/core.py:578 msgid "Password not provided" msgstr "Merci d'indiquer un mot de passe" #: flask_security/core.py:580 #, python-format msgid "Password must be at least %(length)s characters" msgstr "Le mot de passe doit comporter au moins %(length)s caractères" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "Le mot de passe n'est pas assez complexe" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "Ce mot de passe figure dans une liste de mots de passe compromis" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "" "Impossible de contacter le site de vérification des mots de passe " "compromis" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "Numéro de téléphone non valide, par ex. code pays manquant" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "Cet utilisateur n'existe pas" #: flask_security/core.py:591 msgid "Invalid password" msgstr "Mot de passe non valide" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "Le mot de passe ou le code soumis n'est pas valide" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "Vous êtes bien connecté(e)." #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "Mot de passe oublié ?" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" "Vous avez bien réinitialisé votre mot de passe et avez été " "automatiquement connecté(e)." #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" "Vous avez bien réinitialisé votre mot de passe. Veuillez vous " "authentifier avec votre nouveau mot de passe." #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "Votre nouveau mot de passe doit être différent du précédent." #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "Vous avez bien changé votre mot de passe." #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "Veuillez vous connecter pour accéder à cette page." #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "Veuillez vous reconnecter pour accéder à cette page." #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "Le jeton de rafraîchissement n'est pas valide (%(reason)s)" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "Réauthentification réussie" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "" "Vous ne pouvez accéder à cette ressource que si vous n'êtes pas " "connecté(e)." #: flask_security/core.py:622 msgid "Code has been sent." msgstr "Le code a été envoyé." #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "Impossible d'envoyer le code. Veuillez réessayer plus tard" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "Votre code a été confirmé" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "Vous avez bien modifié votre méthode à deux facteurs." #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "" "Vous ne disposez pas actuellement des autorisations nécessaires pour " "accéder à cette page" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "La méthode sélectionnée n'est pas valide" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "Vous avez bien désactivé l'authentification à deux facteurs." #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" "La configuration doit être terminée dans le délai de %(within)s. Veuillez" " recommencer." #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "Options de connexion actuellement actives : %(method_list)s." #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "La méthode demandée n'est pas valide" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "Configuration de la connexion unifiée réussie" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "Vous devez spécifier une identité valide pour vous connecter" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "Utilisez ce code pour vous connecter : %(code)s" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "Vous avez bien changé votre nom d'utilisateur" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" "Le nom d'utilisateur doit comporter au moins %(min)d caractères et moins " "de %(max)d caractères" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "Le nom d'utilisateur ne peut contenir que des lettres et des chiffres" #: flask_security/core.py:667 msgid "Username not provided" msgstr "Merci d'indiquer un nom d'utilisateur" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "Le nom d'utilisateur %(username)s est déjà utilisé." #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" "Les opérations sur les clés d'accès doivent être terminées dans le délai " "de %(within)s. Veuillez recommencer." #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "Un surnom est requis pour la nouvelle clé d'accès." #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "%(name)s est déjà associé à une clé d'accès." #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "%(name)s n'est pas enregistré auprès de l'utilisateur actuel." #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" "Les surnoms des clés d'accès ne peuvent contenir que des lettres, des " "chiffres et une ponctuation limitée." #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "Clé d'accès supprimée : %(name)s" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "Clé d'accès ajoutée : %(name)s" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "Clé d'accès déjà enregistrée." #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "Clé d'accès non enregistrée." #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "Cette clé d'accès n'appartient à aucun utilisateur." #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "Impossible de vérifier la clé d'accès : %(cause)s." #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "Clé d'accès non enregistrée pour cet usage (premier ou secondaire)" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "L'identifiant utilisateur ne correspond pas" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" "La confirmation doit être terminée dans le délai de %(within)s. Veuillez " "recommencer." #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "Changement d'adresse email confirmé" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" "Les instructions de confirmation de votre nouvelle adresse email ont été " "envoyées à %(email)s." #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" "Si vous êtes enregistré(e), votre nom d'utilisateur vous sera envoyé par " "email." #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" "Configuration à l'aide d'une application d'authentification (par exemple," " google, lastpass, authy)" #: flask_security/forms.py:72 msgid "Change Method" msgstr "Changer de méthode" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Changer le mot de passe" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "Code d'authentification" #: flask_security/forms.py:75 msgid "Delete" msgstr "Supprimer" #: flask_security/forms.py:76 msgid "Email Address" msgstr "Adresse email" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "Configuration par email" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "Erreur(s)" #: flask_security/forms.py:79 msgid "Identity" msgstr "Identité" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Connexion" #: flask_security/forms.py:81 msgid "New Password" msgstr "Nouveau mot de passe" #: flask_security/forms.py:82 msgid "Passcode" msgstr "Code d'accès" #: flask_security/forms.py:83 msgid "Password" msgstr "Mot de passe" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "Numéro de téléphone" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "Récupérer le mot de passe" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "Récupérer le nom d'utilisateur" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Inscription" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "Se souvenir de moi" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "Réinitialiser le mot de passe" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "Confirmer le mot de passe" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "Renvoyer les instructions de confirmation" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "Envoyer le lien de connexion" #: flask_security/forms.py:93 msgid "Send Code" msgstr "Envoyer le code" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Connexion" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "Configuration par SMS" #: flask_security/forms.py:96 msgid "Submit" msgstr "Envoyer" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "Soumettre le code" #: flask_security/forms.py:98 msgid "Username" msgstr "Nom d'utilisateur" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "Vérifier le mot de passe" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "Authentificateur Google" #: flask_security/forms.py:105 msgid "authenticator" msgstr "authentificateur" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "email" #: flask_security/forms.py:108 msgid "SMS" msgstr "SMS" #: flask_security/forms.py:109 msgid "password" msgstr "mot de passe" #: flask_security/forms.py:110 msgid "passkey" msgstr "clé d'accès" #: flask_security/forms.py:111 msgid "none" msgstr "aucune" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "Jeton de rafraîchissement" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "Méthodes disponibles" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "Désactiver l'authentification à deux facteurs" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "Des difficultés à accéder à votre compte ? / Appareil mobile perdu ?" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "Contacter l'administrateur" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "Afficher les codes de récupération" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "Générer de nouveaux codes de récupération" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "Code de récupération" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "Méthodes de second facteur disponibles :" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "Sélectionner" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "Envoyer le code par email" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "Utiliser un code de récupération téléchargé précédemment" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "Code ou mot de passe" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "Par email" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "Par SMS" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "Configurer une option de connexion supplémentaire" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "Supprimer l'option de connexion active" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "Surnom" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "Utilisation" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "Utiliser comme premier facteur d'authentification" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "Utiliser comme facteur d'authentification secondaire" #: flask_security/webauthn.py:241 msgid "Start" msgstr "Démarrer" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menu" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "Changer l'adresse email enregistrée" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "Changer le nom d'utilisateur" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "Configuration à deux facteurs" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "Configuration de la connexion unifiée" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "Configuration des clés d'accès" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "Connexion unifiée" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "Confirmer le compte" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "Changer l'adresse email" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" "Une fois cette demande validée, un email de confirmation sera envoyé à " "cette nouvelle adresse email." #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" "Vous n'avez actuellement pas de mot de passe : cette action va en créer " "un." #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "Le nom d'utilisateur actuel est : %(username)s" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Envoyer les instructions de réinitialisation de mot de passe" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "ou" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "Utiliser une clé d'accès pour vous connecter" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "Se connecter avec une clé d'accès" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "Utiliser un réseau social pour vous connecter" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "Continuer avec %(provider)s" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "Confirmer la déconnexion" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "Saisir un code de récupération" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "Codes de récupération" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" "Veillez à les copier et à les conserver en lieu sûr. Chaque code ne peut " "être utilisé qu'une seule fois." #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "Générer de nouveaux codes de récupération" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "Récupération du nom d'utilisateur" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Réinitialiser le mot de passe" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Renvoyer les instructions de confirmation" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "Sélectionner une méthode à deux facteurs" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" "L'authentification à deux facteurs ajoute une couche de sécurité " "supplémentaire à votre compte" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" "En plus de votre nom d'utilisateur et de votre mot de passe, vous devrez " "utiliser un code." #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "Méthode à deux facteurs actuellement configurée : %(method)s" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" "Ouvrez une application d'authentification sur votre appareil et scannez " "le code QR suivant (ou saisissez le code ci-dessous manuellement) pour " "commencer à recevoir des codes :" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "Code d'authentification à deux facteurs" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "Saisissez le code pour terminer la configuration" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "saisissez le code numérique" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "Cette application permet de configurer des codes de récupération." #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "Vous pouvez les paramétrer ici." #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "Clés d'accès" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "Cette application prend en charge les clés d'accès." #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "Authentification à deux facteurs" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "Veuillez saisir votre code d'authentification généré via : %(method)s" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "Le code d'authentification a été envoyé à votre adresse email" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" "Un email nous a été envoyé afin de réinitialiser le compte de votre " "application" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "Configuration de la connexion unifiée" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Code QR sans mot de passe" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "Aucune méthode n'a été activée : rien à configurer" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "Saisissez le code ici pour terminer la configuration" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "Demander l'envoi d'un code à usage unique" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "Réauthentification" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Le code a été envoyé" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "Utiliser une clé d'accès pour vous reconnecter" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "Utiliser un réseau social pour vous reconnecter" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "Configurer une nouvelle clé d'accès" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "Commencez par indiquer un nom unique pour votre clé d'accès :" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "Clés d'accès actuellement enregistrées :" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" "Surnom : \"%s\" Utilisation : \"%s\" Transports : \"%s\" Détectable : " "\"%s\" Type d'appareil : \"%s\" Sauvegardée ? \"%s\" Dernière utilisation" " le : %s" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "Supprimer une clé d'accès existante" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "Connexion avec une clé d'accès" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "Connexion avec une clé d'accès" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "Utilisation d'une clé d'accès comme second facteur" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "Réauthentification avec une clé d'accès" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" "Utilisez ce lien pour confirmer votre nouvelle " "adresse email." #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "Ce lien expirera dans %(within)s." #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "Votre adresse email actuellement enregistrée est %(email)s." #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "Utilisez %(link)s pour confirmer votre nouvelle adresse email." #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Votre mot de passe a été changé." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Si vous n'avez pas changé votre mot de passe," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "cliquez ici pour le réinitialiser" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" "Si vous n'avez pas changé votre mot de passe, cliquez sur le lien ci-" "dessous pour le réinitialiser." #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "Votre nom d'utilisateur a été changé." #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" "Utilisez ce lien pour confirmer " "votre adresse email." #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "Utilisez %(confirmation_link)s pour confirmer votre adresse email." #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Bienvenue %(email)s !" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Vous pouvez vous connecter via le lien ci-dessous :" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Se connecter maintenant" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" "Utilisez ce lien pour réinitialiser votre " "mot de passe." #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "Utilisez %(reset_link)s pour réinitialiser votre mot de passe." #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "Votre mot de passe a été réinitialisé." #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "Bienvenue %(username)s !" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" "Vous pouvez vous connecter à votre compte à l'aide du code suivant : " "%(token)s" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "Ce code expirera dans %(within)s." #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "impossible d'accéder au compte de messagerie" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" "Vous pouvez vous connecter à votre compte à l'aide du code suivant : " "%(code)s" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "Ou utilisez ce lien : Connexion" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "Ce code/lien expirera dans %(within)s." #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "Ou utilisez ce lien : %(login_link)s" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "Bonjour," #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "Vous avez récemment demandé à récupérer votre nom d'utilisateur." #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "Votre nom d'utilisateur est : %(username)s" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" "Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer cet " "email en toute sécurité." #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "Bonjour %(email)s !" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" "Quelqu'un (vous ?) a tenté de s'inscrire avec cet email, qui est déjà " "présent dans notre système." #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" "Ce compte est également associé au nom d'utilisateur suivant : " "%(username)s." #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" "Vous pouvez utiliser ce lien pour " "réinitialiser votre mot de passe." #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" "Vous n'avez pas encore confirmé votre adresse email - utilisez ce lien pour le faire maintenant." #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" "Ce compte est également associé au nom d'utilisateur suivant : " "%(username)s" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" "Vous pouvez utiliser ce lien %(reset_link)s pour réinitialiser votre mot " "de passe." #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" "Vous n'avez pas encore confirmé votre adresse email - utilisez ce lien : " "%(confirmation_link)s pour le faire maintenant." #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" "Vous avez tenté de vous inscrire avec un nom d'utilisateur " "\"%(username)s\" qui est déjà associé à un autre compte." #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" "Veuillez recommencer le processus d'inscription avec un nom d'utilisateur" " différent." pallets-eco-flask-security-72bf929/flask_security/translations/hu_HU/000077500000000000000000000000001525721743500260265ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/hu_HU/LC_MESSAGES/000077500000000000000000000000001525721743500276135ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/hu_HU/LC_MESSAGES/flask_security.po000066400000000000000000001350011525721743500332020ustar00rootroot00000000000000# Hungarian (Hungary) translations for Flask-Security. # Copyright (C) 2022 ORGANIZATION # This file is distributed under the same license as the Flask-Security # project. # FIRST AUTHOR ritt.alex@gmail.com, 2022. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 4.0.0\n" "Report-Msgid-Bugs-To: jwag956@github.com\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: hu_HU\n" "Language-Team: hu_HU \n" "Plural-Forms: nplurals=1; plural=0;\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:363 msgid "Login Required" msgstr "Bejelentkezés szükséges" #: flask_security/core.py:364 msgid "Welcome" msgstr "Üdvözöljük" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "Kérjük, erősítse meg e-mail címét" #: flask_security/core.py:366 msgid "Login instructions" msgstr "Bejelentkezési utasítások" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "A jelszava visszaállításra került" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "A jelszava megváltozott" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "Jelszó visszaállítási utasítások" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:371 msgid "Your requested username" msgstr "" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:423 msgid "Verification Code" msgstr "Ellenőrző kód" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "A bemenet nem megfelelő a kért API-hoz" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "Sikertelen hitelesítés - azonosító vagy jelszó/kód érvénytelen" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" "Ha ez az e-mail cím szerepel a rendszerünkben, akkor kapni fog egy " "e-mailt a jelszó visszaállítási menetéről." #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "Ha ez az azonosító szerepel a rendszerünkben, akkor kódot küldtünk." #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "Nincs jogosultsága ennek az erőforrásnak a megtekintéséhez." #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "Köszönjük. E-mail címét megerősítettük." #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "Az e-mail címét már megerősítették." #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "Érvénytelen megerősítő token." #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s már társítva van egy fiókhoz." #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" "A \"%(attr)s\" azonosító attribútum \"%(value)s\" értékkel már társítva " "van egy fiókhoz." #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:531 msgid "Password does not match" msgstr "A jelszó nem egyezik" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "A jelszavak nem egyeznek" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "A tartományon kívüli átirányítások tilosak" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "A helyreállítási kód érvénytelen" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "" "A jelszó visszaállítására vonatkozó utasításokat elküldtük a következő " "címre: %(email)s." #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "Érvénytelen jelszó-visszaállítási token." #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "Az e-mail megerősítést igényel." #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "A megerősítő utasításokat elküldtük a következő címre: %(email)s." #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Nem jelentkezett be %(within)s-en belül. Új bejelentkezési utasításokat " "küldtünk a következő címre: %(email)s." #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "" "A bejelentkezéshez szükséges utasításokat elküldtük a következő címre: " "%(email)s." #: flask_security/core.py:565 msgid "Invalid login token." msgstr "Érvénytelen bejelentkezési token." #: flask_security/core.py:566 msgid "Account is disabled." msgstr "A fiók le van tiltva." #: flask_security/core.py:567 msgid "Email not provided" msgstr "E-mail nincs megadva" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "Érvénytelen e-mail cím" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "Érvénytelen kód" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "" #: flask_security/core.py:578 msgid "Password not provided" msgstr "Nincs megadva a jelszó" #: flask_security/core.py:580 #, python-format msgid "Password must be at least %(length)s characters" msgstr "A jelszónak legalább %(length)s karakterből kell állnia" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "A jelszó nem elég összetett" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "A telefonszám nem érvényes, pl. hiányzik az országkód" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "A megadott felhasználó nem létezik" #: flask_security/core.py:591 msgid "Invalid password" msgstr "Érvénytelen jelszó" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "A beküldött jelszó vagy kód érvénytelen" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "Sikeresen bejelentkezett." #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "Elfelejtette jelszavát?" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "Sikeresen visszaállította jelszavát, és automatikusan be is jelentkezett." #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "Az új jelszavának különböznie kell a korábbi jelszavától." #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "Sikeresen megváltoztatta a jelszavát." #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "Kérjük, jelentkezzen be az oldal eléréséhez." #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "Kérjük, hitelesítse újra az oldal eléréséhez." #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "Az újrahitelesítés sikeres" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "\"Csak akkor érheti el ezt a végpontot, ha nincs bejelentkezve." #: flask_security/core.py:622 msgid "Code has been sent." msgstr "A kód elküldve." #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "Nem sikerült elküldeni a kódot. Kérjük, próbálja újra később." #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "A kódod megerősítésre került" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "Sikeresen megváltoztatta a kétfaktoros hitelesítést." #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "Jelenleg nincs jogosultsága az oldal eléréséhez" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "A megjelölt metódus nem érvényes" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "A telepítést %(within)s időn belül be kell fejezni. Kezdje újra." #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "A kért metódus nem érvényes" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "Az egységes bejelentkezés beállítása sikeres" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "A bejelentkezéshez érvényes azonositót kell megadnia" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" "A felhasználónévnek legalább %(min)d karakterből és kevesebb mint %(max)d" " karakterből kell állnia" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "A felhasználónév csak betűket és számokat tartalmazhat" #: flask_security/core.py:667 msgid "Username not provided" msgstr "A felhasználónév nincs megadva" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "%(username)s már hozzá van rendelve egy fiókhoz." #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "%(name)s nincs regisztrálva az aktuális felhasználónál." #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "Beállítás hitelesítő alkalmazás segítségével (pl. google, lastpass, authy)" #: flask_security/forms.py:72 msgid "Change Method" msgstr "Módszer módosítása" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Jelszó módosítása" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "Hitelesítési kód" #: flask_security/forms.py:75 msgid "Delete" msgstr "Törlés" #: flask_security/forms.py:76 msgid "Email Address" msgstr "E-mail" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "Beállítás e-mail használatával" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "Hiba(k)" #: flask_security/forms.py:79 msgid "Identity" msgstr "Azonosító" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Bejelentkezés" #: flask_security/forms.py:81 msgid "New Password" msgstr "Új jelszó" #: flask_security/forms.py:82 msgid "Passcode" msgstr "Kód" #: flask_security/forms.py:83 msgid "Password" msgstr "Jelszó" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "Telefonszám" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "Jelszó helyreállítása" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Regisztráció" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "Emlékezz rám" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "Jelszó visszaállítása" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "Jelszó megerősítése" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "Megerősítő utasítások újraküldése" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "Bejelentkezési link küldése" #: flask_security/forms.py:93 msgid "Send Code" msgstr "Kód küldése" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Bejelentkezés" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "Beállítás SMS-sel" #: flask_security/forms.py:96 msgid "Submit" msgstr "Küldés" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "Kód beküldése" #: flask_security/forms.py:98 msgid "Username" msgstr "Felhasználónév" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "Jelszó ellenőrzése" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:105 msgid "authenticator" msgstr "" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "" #: flask_security/forms.py:108 msgid "SMS" msgstr "" #: flask_security/forms.py:109 msgid "password" msgstr "" #: flask_security/forms.py:110 msgid "passkey" msgstr "" #: flask_security/forms.py:111 msgid "none" msgstr "" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "Elérhető módszerek" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "Helyreállítási kódok megjelenítése" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "Új helyreállítási kódok generálása" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "Helyreállítási kód" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "Elérhető második faktoros módszerek:" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "Kiválasztás" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "Kód vagy jelszó" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "E-mailben" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "SMS-ben" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "Kiegészítő bejelentkezési lehetőség beállítása" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "Az aktív bejelentkezési opció törlése" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "Becenév" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "Használat" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "Használat első hitelesítési lépésként" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "Használat másodlagos hitelesítési lépésként" #: flask_security/webauthn.py:241 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menü" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "Egységes bejelentkezés beállítása" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "Egységes bejelentkezés" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "Fiók megerősítése" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "Jelenleg nincs jelszava – ez hozzáad egyet." #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Jelszó visszaállítási utasítások küldése" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "vagy" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "Adja meg a helyreállítási kódot" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "Helyreállítási kódok" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" "Mindenképpen másolja át ezeket, és tárolja biztonságos helyen. Minden kód" " csak egyszer használható." #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Jelszó visszaállítása" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Megerősítő utasítások újraküldése" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "A felhasználóneve és a jelszava mellett egy kódot is kell használnia." #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" "Nyisson meg egy hitelesítő alkalmazást eszközén, és olvassa be a " "következő QR-kódot (vagy írja be kézzel az alábbi kódot), hogy megkezdje " "a kódok fogadását:" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "Ez az alkalmazás támogatja a helyreállítási kódok beállítását." #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "Itt állíthatja be." #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" "Kérjük, adja meg hitelesítési kódját, amelyet a következővel generált: " "%(method)s" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "A hitelesítési kódot elküldtük az Ön e-mail címére" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "Egységes bejelentkezés beállítása" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Jelszó nélküli QRCode" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "Nincs engedélyezve metódus – nincs mit beállítani" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "A beállítás befejezéséhez írja be ide a kódot" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "Kérje egyszeri kód küldését" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "A kód elküldve" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" "Becenév: \"%s\" Használat: \"%s\" Szállítás: \"%s\" Felfedezhető: \"%s\" " "Eszköz típusa: \"%s\" Biztonsági mentés készült? \"%s\" Utolsó használat:" " %s" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "A jelszava megváltozott." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Ha nem változtatta meg jelszavát," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "kattints ide a visszaállításhoz" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" "Ha nem változtatta meg jelszavát, kattintson az alábbi linkre a " "visszaállításhoz." #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "" #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Üdvözöljük %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Az alábbi linken keresztül jelentkezhet be fiókjába:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Bejelentkezés most" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "nem tud hozzáférni a mail fiókhoz" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "Szia %(email)s!" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" "Valaki megpróbálta regisztrálni ezt az e-mailt - amely már a " "rendszerünkben van." #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "Ehhez a fiókhoz a következő felhasználónév is tartozik: %(username)s." #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "Ehhez a fiókhoz a következő felhasználónév is tartozik: \"%(username)s" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" "Olyan felhasználónévvel próbált meg regisztrálni: \"%(username)s\", amely" " már társítva van egy másik fiókhoz." #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" "Kérjük, indítsa újra a regisztrációs folyamatot egy másik " "felhasználónévvel." #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "Nem állította vissza jelszavát %(within)s " #~ "időn belül. Új utasításokat küldtünk a" #~ " következő címre: %(email)s." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "Nem erősítette meg e-mail-címét " #~ "%(within)s-en belül. Az e-mail megerősítéséhez" #~ " új utasításokat küldtünk a következő " #~ "címre: %(email)s." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "Ön nincs hitelesítve. Kérjük, adja meg a megfelelő hitelesítő adatokat." #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "A bejelentkezés befejezéséhez adja meg az e-mail címére küldött kódot" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "Melyik telefonszámra küldjük a kódot?" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ "E-mailt küldtünk nekünk az alkalmazás " #~ "fiókjának visszaállítása érdekében" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "" #~ "Köszönjük. A megerősítő utasításokat elküldtük" #~ " a következő címre: %(email)s." #~ msgid "Two-factor Login" #~ msgstr "Kétfaktoros Bejelentkezés" #~ msgid "Two-factor Rescue" #~ msgstr "Kétfaktoros Visszaállítás" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "A végpont eléréséhez újra hitelesíteni kell" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "Sikeresen letiltotta a kétfaktoros hitelesítést." #~ msgid "Disable two factor authentication" #~ msgstr "Kétfaktoros hitelesítés letiltása" #~ msgid "Two Factor Setup" #~ msgstr "Kétfaktoros beállítás" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "Kétfaktoros módszer kiválasztása" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "A kétfaktoros hitelesítés extra biztonsági réteget ad fiókjához" #~ msgid "Two factor authentication code" #~ msgstr "Kétfaktoros hitelesítési kód" #~ msgid "Two-factor Authentication" #~ msgstr "Kétfaktoros Hitelesítés" #~ msgid "Please Reauthenticate" #~ msgstr "Kérem hitelesítse újra" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "Kérjük, hitelesítsen újra a WebAuthn biztonsági kulcs használatával" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Jelszó módosítása" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Fiók megerősítése" #~ msgid "You can log into your account using the following code:" #~ msgstr "A következő kóddal jelentkezhet be fiókjába:" #~ msgid "You can sign into your account using the following code:" #~ msgstr "A következő kóddal jelentkezhet be fiókjába:" #~ msgid "Or use the link below:" #~ msgstr "Vagy használja az alábbi linket:" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Kérjük, erősítse meg e-mail címét az alábbi linken keresztül:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Megerősítheti e-mailjét az alábbi linken keresztül:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "Ha elfelejtette jelszavát, visszaállíthatja" #~ msgid " here." #~ msgstr " ezen a linken." #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "Ha elfelejtette jelszavát, a következő linken visszaállíthatja:" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "Használja ezt a kódot a bejelentkezéshez: %(code)s." #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ "A WebAuthn műveletet %(within)s időn " #~ "belül be kell fejezni. Kezdje újra." #~ msgid "Nickname for new credential is required." #~ msgstr "Az új hitelesítő adatokhoz becenév szükséges." #~ msgid "%(name)s is already associated with a credential." #~ msgstr "%(name)s már hozzá van rendelve egy hitelesítő adathoz." #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ "A WebAuthn hitelesítő adatok sikeresen " #~ "törölve a következő névvel: %(name)s" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ "Sikeresen hozzáadva a WebAuthn hitelesítő " #~ "adatot a következő névvel: %(name)s" #~ msgid "WebAuthn credential id already registered." #~ msgstr "A WebAuthn hitelesítő adatazonosítója már regisztrálva." #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "Nem regisztrált WebAuthn hitelesítő adat azonosítója." #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "A WebAuthn hitelesítő adatok nem tartoznak egyetlen felhasználóhoz sem." #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "Nem sikerült ellenőrizni a WebAuthn hitelesítő adatait: %(cause)s." #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ "A hitelesítő adat nincs regisztrálva " #~ "ehhez a felhasználáshoz (első vagy " #~ "másodlagos)" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "\"WebAuthn beállítása" #~ msgid "Forgot password" #~ msgstr "Elfelejtett jelszó" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "A WebAuthn használata a bejelentkezéshez" #~ msgid "Sign in with WebAuthn" #~ msgstr "Bejelentkezés WebAuthn segítségével" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "Ez az alkalmazás támogatja a WebAuthn biztonsági kulcsokat." #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "Használjon WebAuthn biztonsági kulcsot az újrahitelesítéshez" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "Új WebAuthn biztonsági kulcs beállítása" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "Először adjon meg egy egyedi nevet az új biztonsági kulcsnak:" #~ msgid "Currently registered security keys:" #~ msgstr "Jelenleg regisztrált biztonsági kulcsok:" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "Meglévő WebAuthn biztonsági kulcs törlése" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "Bejelentkezés WebAuthn biztonsági kulcs használatával" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "Használja a WebAuthn biztonsági kulcsát második lépésként" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider: (%(exerror)s -" #~ " %(exdesc)s). Please try again." #~ msgstr "" #~ msgid "Use Social Oauth to Sign In" #~ msgstr "" #~ msgid "Sign in with %(provider)s" #~ msgstr "" #~ msgid "Username contains illegal characters" #~ msgstr "A felhasználónév illegális karaktereket tartalmaz" #~ msgid "Sign out" #~ msgstr "Kijelentkezés" #~ msgid "Click here to reset your password" #~ msgstr "Kattintson ide a jelszó visszaállításához" #~ msgid "Click the link below to reset your password:" #~ msgstr "Kattintson az alábbi linkre a jelszó visszaállításához:" #~ msgid "You can sign in to your account using the following code: %(token)s" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s." #~ msgstr "" pallets-eco-flask-security-72bf929/flask_security/translations/hy_AM/000077500000000000000000000000001525721743500260135ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/hy_AM/LC_MESSAGES/000077500000000000000000000000001525721743500276005ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/hy_AM/LC_MESSAGES/flask_security.po000066400000000000000000001515331525721743500331770ustar00rootroot00000000000000# Armenian (Armenia) translations for Flask-Security. # Copyright (C) 2020 ORGANIZATION # This file is distributed under the same license as the Flask-Security # project. # FIRST AUTHOR , 2020. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 4.0.0\n" "Report-Msgid-Bugs-To: jwag956@github.com\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: 2020-12-01 11:47+0400\n" "Last-Translator: FULL NAME \n" "Language: hy_AM\n" "Language-Team: hy_AM \n" "Plural-Forms: nplurals=1; plural=0;\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:363 msgid "Login Required" msgstr "Անհրաժեշտ է մուտք գործել" #: flask_security/core.py:364 msgid "Welcome" msgstr "Բարի գալուստ" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "Հաստատեք �եր էլ․փոստի հաս�են" #: flask_security/core.py:366 msgid "Login instructions" msgstr "Մուտքի հրահանգներ" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "�եր գաղտնաբառը վերականգնվել է" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "�եր գաղտնաբառը փոխվե�" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "Գաղտնաբառի վերականգնման հրահանգներ" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:371 msgid "Your requested username" msgstr "" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:423 msgid "Verification Code" msgstr "Վերահաստատման ծածկագիր" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "Մուտքը չի համապատասխանում հայ�վող API֊ին" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "Նույնականա�ումը ձախողվե�. ինքնությունը կամ գաղտնաբառը/ծածկագիրը անվավեր է" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" "Եթե այդ էլ․փոստի հաս�են գտնվում է մեր համակարգում, դուք կստանաք նամակ, " "որտեղ նկարագրված է, թե ինչպես վերականգնել ձեր գաղտնաբառը:" #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "Եթե այդ ինքնությունը մեր համակարգում է, ձեզ ծածկագիր է ուղարկվել:" #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "Դուք այս ռեսուրսը դիտելու թույլտվություն չունեք" #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "Շնորհակալություն. �եր էլ․փոստի հաս�են հաստատվել է։" #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "�եր էլ․փոստի հաս�են արդեն հաստատված է։" #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "Հաստատման տոկենը անվավեր է։" #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s արդեն կապված է այլ օգտահաշվի հետ։" #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" "«%(value)s» արժեքով «%(attr)s» ինքնության հատկանիշն արդեն կապված է հաշվի " "հետ:" #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "«%(id)s» ինքնությունը գրան�ված չէ" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:531 msgid "Password does not match" msgstr "Գաղտնաբառը չի համապատասխանում" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "Գաղտնաբառերը չեն համապատասխանում" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "Դոմենի� դուրս վերահղումներն արգելափակված են" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "Վերականգնման ծածկագիրը անվավեր է" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "Վերականգնման ծածկագրեր դեռ չեն ստեղծվել" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "Գաղտնաբառի վերականգնման հրահանգներն ուղարկվել են %(email)s հաս�եին։" #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "Դուք չեք վերականգնել ձեր գաղտնաբառը %(within)s-ի ընթա�քում:" #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "Գաղտնաբառի վերականգնման տոկենը անվավեր է։" #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "Էլ․փոստի հաս�են պետք է հաստատել։" #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Հաստատման հրահանգները ուղարկվել են %(email)s հաս�եին։" #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "Դուք չեք հաստատել ձեր էլ․փոստի հաս�են %(within)s-ի ընթա�քում:" #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Դուք մուտք չեք գործել %(within)s֊ի ընթա�քում. Մուտքի նոր հրահանգները " "ուղարկվել են %(email)s հաս�եին։" #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Մուտքի հրահանգները ուղարկվել են %(email)s հաս�եին։" #: flask_security/core.py:565 msgid "Invalid login token." msgstr "Մուտքի անվավեր տոկեն։" #: flask_security/core.py:566 msgid "Account is disabled." msgstr "Օգտահաշիվն արգելափակված է։" #: flask_security/core.py:567 msgid "Email not provided" msgstr "Էլ․փոստի հաս�են տրամադրված չէ" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "Անվավեր էլ․փոստի հաս�ե" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "Անվավեր ծածկագիր" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "" #: flask_security/core.py:578 msgid "Password not provided" msgstr "Գաղտնաբառը տրամադրված չէ" #: flask_security/core.py:580 #, python-format msgid "Password must be at least %(length)s characters" msgstr "Գաղտնաբառը պետք է պարունակի առնվազն %(length)s նիշ" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "Գաղտնաբառը բավարար բարդ չէ" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "Գաղտնաբառը բա�ված գաղտնաբառերի �ու�ակում է" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "Չհաջողվե� կապվել բա�ված գաղտնաբառերի կայքի հետ" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "Հեռախոսահամարը անվավեր է, օրինակ� բա�ակայում է երկրի կոդը" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "Նշված օգտատերը գոյություն չունի" #: flask_security/core.py:591 msgid "Invalid password" msgstr "Անվավեր գաղտնաբառ" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "Ներկայա�ված գաղտնաբառը կամ ծածկագիրը վավեր չէ" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "Դուք բարեհաջող մուտք էք գործել։" #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "Մոռա�ել ե՞ք գաղտնաբառը" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" "Դուք հաջողությամբ վերականգնել եք ձեր գաղտնաբառը եւ մուտք էք գործել " "համակարգ ինքնաբերաբար։" #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" "Դուք հաջողությամբ վերականգնել եք ձեր գաղտնաբառը: Խնդրում ենք " "նույնականա�նել� օգտագործելով ձեր նոր գաղտնաբառը:" #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "�եր նոր գաղտնաբառը պետք է տարբերվի նախկինի�" #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "Դուք հաջողությամբ փոխե�իք ձեր գաղտնաբառը։" #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "Մուտք գործեք այս էջի� օգտվելու համար։" #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "Այս էջ մուտք գործելու համար անհրաժեշտ է նորի� նույնականա�վել:" #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "Նույնականա�ումը հաջողված է" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "Դուք կարող եք մտնել այս վերջնակետ միայն այն ժամանակ, երբ մուտք չեք գործել" #: flask_security/core.py:622 msgid "Code has been sent." msgstr "Ծածկագիրը ուղարկվել է։" #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "Չհաջողվե� ուղարկել ծածկագիրը: Խնդրում ենք փորձել ավելի ուշ" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "�եր ծածկագիրը հաստատվե�" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "Դուք հաջողությամբ փոխե�իք ձեր երկու֊գործոն տարբերակը" #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "Ներկայումս դուք չունեք այս էջ մուտք գործելու թույլտվություն" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "Նշված տարբերակը վավեր չէ" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "Կարգավորումը պետք է ավարտվի %(within)s ընթա�քում։ Խնդրում ենք նորի� սկսել։" #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "Հայ�վող մեթոդը վավեր չէ" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "Միասնական մուտքի տեղադրումը հաջող է" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "Մուտք գործելու համար պետք է նշեք վավեր ինքնություն" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "Օգտանունը պետք է լինի առնվազն %(min)d նիշ և %(max)d նիշի� պակաս" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "Օգտանունը կարող է պարունակել միայն տառեր և թվեր" #: flask_security/core.py:667 msgid "Username not provided" msgstr "Օգտանուն չի տրամադրվել" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "%(username)s արդեն կապված է օգտահաշվի հետ:" #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "%(name)s գրան�ված չէ ընթա�իկ օգտատիրոջ մոտ:" #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "Հավատարմագրերի օգտատիրոջ կապը չի համընկնում" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" "Կարգավորեք օգտագործելով վավերա�ման ծրագիր (ինչպիսիք են� google, lastpass," " authy)" #: flask_security/forms.py:72 msgid "Change Method" msgstr "Փոխել տարբերակը" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Փոխել գաղտնաբառը" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "Նույնականա�ման ծածկագիր" #: flask_security/forms.py:75 msgid "Delete" msgstr "Ջնջել" #: flask_security/forms.py:76 msgid "Email Address" msgstr "Էլեկտրոնային փոստի հաս�ե" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "Կարգավորեք էլ․փոստի միջո�ով" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "�խալ" #: flask_security/forms.py:79 msgid "Identity" msgstr "Ինքնություն" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Մուտք" #: flask_security/forms.py:81 msgid "New Password" msgstr "Նոր գաղտնաբառ" #: flask_security/forms.py:82 msgid "Passcode" msgstr "Գաղտնագիր" #: flask_security/forms.py:83 msgid "Password" msgstr "Գաղտնաբառ" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "Հեռախոսահամար" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "Վերականգնել գաղտնաբառը" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Գրան�վել" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "Հիշիր ինձ" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "Զրոյա�նել գաղտնաբառը" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "Կրկին մուտքագրել գաղտնաբառը" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "Վերստին հաստատեք �ու�ումները" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "Ուղարկել մուտքի հղումը" #: flask_security/forms.py:93 msgid "Send Code" msgstr "Ուղարկել ծածկագիր" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Մուտք գործել" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "Կարգավորեք օգտագործելով SMS" #: flask_security/forms.py:96 msgid "Submit" msgstr "Ներկայա�նել" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "Ներկայա�նել ծածկագիր" #: flask_security/forms.py:98 msgid "Username" msgstr "Օգտատեր" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "�տուգեք գաղտնաբառը" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:105 msgid "authenticator" msgstr "" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "" #: flask_security/forms.py:108 msgid "SMS" msgstr "" #: flask_security/forms.py:109 msgid "password" msgstr "" #: flask_security/forms.py:110 msgid "passkey" msgstr "" #: flask_security/forms.py:111 msgid "none" msgstr "" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "Առկա տարբերակներ" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "Ցույ� տալ վերականգնման ծածկագրերը" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "�տեղծեք վերականգնման նոր ծածկագրեր" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "Վերականգնման ծածկագիր" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "Երկրորդ գործոնի հասանելի մեթոդներ." #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "Ընտրել" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "Ծածկագիր կամ Գաղտնաբառ" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "Էլ․ Փոստով" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "SMS հաղորդագրությամբ" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "Կարգավորեք մուտքի լրա�ու�իչ տարբերակ" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "Ջնջել ակտիվ մուտքի տարբերակը" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "Մականուն" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "Կիրառում" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "Օգտագործեք որպես նույնականա�ման առաջին գործոն" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "Օգտագործեք որպես նույնականա�ման երկրորդական գործոն" #: flask_security/webauthn.py:241 msgid "Start" msgstr "�կսել" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Ընտրա�անք" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "Միասնական մուտքի կարգավորում" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "Միասնական Մուտք" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "Հաստատել օգտահաշիվը" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "Դուք ներկայումս գաղտնաբառ չունեք, սա կավելա�վի:" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Ուղարկել գաղտնաբառի վերականգնման հրահանգները" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "կամ" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "Մուտքագրեք վերականգնման ծածկագիրը" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "Վերականգնման ծածկագրեր" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" "Համոզվեք, որ պատճենեք դրանք և պահեք ապահով տեղում: Յուրաքանչյուր ծածկագիր" " կարող է օգտագործվել միայն մեկ անգամ:" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "�տեղծեք վերականգնման նոր ծածկագրեր" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Վերականգնել գաղտնաբառը" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Ուղարկել օգտահաշվի վերահաստատման հրահանգները" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "Բա�ի ձեր օգտանունի� և գաղտնաբառի�, դուք պետք է օգտագործեք ծածկագիր:" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "Ներկայիս կարգավորված երկգործոն մեթոդը. %(method)s" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" "Բա�եք վավերա�նող հավելվածը ձեր սարքում և սկանավորեք հետևյալ QRcode-ը (կամ" " մուտքագրեք ստորև նշված ծածկագիրը) ծածկագրեր ստանալու համար." #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "Այս հավելվածն օժանդակում է վերականգնման ծածկագրերի կարգավորումը:" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "Դուք կարող եք դրանք կարգավորել այստեղ:" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" "Խնդրում ենք մուտքագրել ձեր նույնականա�ման ծածկագիրը, որը ստեղծվել է " "%(method)s միջո�ով." #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "Նույնականա�ման ծածկագիրն ուղարկվել է �եր էլ․հաս�եին" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "Կարգավորեք միասնական մուտքը" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Առան� գաղտնաբառի QRcode" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "Ոչ մի տարբերակի հնարավորություն տրված չէ ֊ կարգաբերելու կարիք չկա" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "Մուտքագրեք ծածկագիրն այստեղ� կարգավորումն ավարտելու համար" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "Հայ�եք միանգամյա ծածկագրի ուղարկում" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Ծածկագիրն ուղարկվել է" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" "Մականուն� \"%s\" Օգտագործում� \"%s\" Փոխադրումներ� \"%s\" Հայտնաբերելի� " "\"%s\" �արքի տեսակ� \"%s\" Պահուստավորվե՞լ է: \"%s\" Վերջին անգամ " "օգտագործվել է� %s" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "�եր գաղտնաբառը փոխվել է" #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "�եր գաղտնաբառը եթե դուք չեք փոխել," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "այն վերականգնելու համար սեղմեք այստեղ" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "Եթե դուք չեք փոխել �եր գաղտնաբառը, սեղմեք հղումին, որպեսզի փոխեք այն" #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "" #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Բարի գալուստ %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Դուք կարող եք մուտք գործել �եր օգտահաշիվ ստորև նշված հղումով." #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Մուտք գործել հիմա" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "Օգտահաշիվ հնարավոր չէ մուտք գործել" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "Բարև %(email)s" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" "Ինչ-որ մեկը (դու՞ք) փորձել է գրան�ել այս էլփոստը, որն արդեն մեր " "համակարգում է:" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "Այս հաշիվը ունի նաև հետևյալ օգտվողի անունը� կապված դրա հետ. %(username)s։" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "Այս հաշիվը ունի նաև հետևյալ օգտվողի անունը� կապված իրա հետ. %(username)s" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" "Դուք փորձել եք գրան�վել \"%(username)s\" օգտանունով, որը արդեն կապված է " "մեկ այլ հաշվի հետ:" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "Խնդրում ենք վերսկսել գրան�ման գործընթա�ը այլ օգտվողի անունով:" #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "Դուք նույնականա�ված չեք: Խնդրում ենք մուտքագրել ճիշտ տվյալներ։" #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "Ներկայում ակտիվ մուտքի տարբերակներ." #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ "Մուտքն ավարտելու համար մուտքագրեք ձեր " #~ "էլեկտրոնային հաս�եին ուղարկված ծածկագիրը" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "Ո՞ր հեռախոսահամարին պետք է ուղարկել ծածկագիրը" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "�եր օգտահաշվի կիրառումը վերականգնելու համար մեզ նամակ է ուղարկվել" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "Oauth մատակարարի հետ կապի սխալ է տեղի ունե�ել: Խնդրում եմ կրկին փորձեք." #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "Շնորհակալություն. Հաստատման հրահանգներն ուղարկվել են %(email)s հաս�եին։" #~ msgid "Two-factor Login" #~ msgstr "Երկու գործոնով մուտք" #~ msgid "Two-factor Rescue" #~ msgstr "Երկու գործոնով վերականգնում" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Դուք պետք է կրկին նույնական�վեք որպեսզի այստեղ մուտք գործեք" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "Դուք հաջողությամբ անջատել եք երկու գործոնի թույլտվությունը" #~ msgid "Disable two factor authentication" #~ msgstr "Անջատել երկու գործոնով նույնականա�ումը" #~ msgid "Two Factor Setup" #~ msgstr "Երկու գործոնի կարգավորում" #~ msgid "Sign in with " #~ msgstr "Մուտք գործեք " #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "Ընտրեք «երկու գործոնի» տարբերակը" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ "Երկու գործոնով նույնականա�ումը ձեր հաշվին " #~ "ավելա�նում է անվտանգության լրա�ու�իչ շերտ" #~ msgid "Two factor authentication code" #~ msgstr "Երկու գործոն նույնականա�ման ծածկագիր" #~ msgid "Two-factor Authentication" #~ msgstr "Երկու գործոն նույնականա�ում" #~ msgid "Please Reauthenticate" #~ msgstr "Խնդրում եմ նորի� վերանույնական�վեք:" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ "Խնդրում ենք վերանույնական�վեք� օգտագործելով " #~ "ձեր WebAuthn անվտանգության բանալին" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Փոխել գաղտնաբառը" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Հաստատել իմ օգտահաշիվը" #~ msgid "You can log into your account using the following code:" #~ msgstr "Դուք կարող եք մտնել �եր օգտահաշիվ� օգտագործելով հետևյալ ծածկագիրը." #~ msgid "You can sign into your account using the following code:" #~ msgstr "Կարող եք մուտք գործել �եր օգտահաշիվ� օգտագործելով հետևյալ ծածկագիրը." #~ msgid "Or use the link below:" #~ msgstr "Կամ օգտագործեք ստորեւ նշված հղումը" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "�տորև բերված հղումով հաստատեք ձեր էլ. փոստի հաս�են․" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Դուք կարող եք հաստատել ձեր էլ. փոստի հաս�են ստորև նշված հղումով." #~ msgid "If you forgot your password you can reset it" #~ msgstr "Եթե մոռա�ել եք ձեր գաղտնաբառը, կարող եք վերականգնել այն" #~ msgid " here." #~ msgstr " այստեղ։" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ "Եթե մոռա�ել եք ձեր գաղտնաբառը, կարող " #~ "եք վերականգնել այն հետևյալ հղումով." #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "Մուտքի համար օգտագործեք այս ծածկագիրը․ %(code)s։" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ "WebAuthn գործողությունը պետք է ավարտվի " #~ "%(within)s։ Խնդրում եմ սկսել նորի�:" #~ msgid "Nickname for new credential is required." #~ msgstr "Նոր հավատարմագրի մականունը պարտադիր է:" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "%(name)s-ն արդեն կապված է հավատարմագրի հետ:" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "%(name)s անունով WebAuthn հավատարմագիրը հաջողությամբ ջնջվե�" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "%(name)s անունով WebAuthn հավատարմագիրը հաջողությամբ ավելա�վե�" #~ msgid "WebAuthn credential id already registered." #~ msgstr "WebAuthn հավատարմագրի ID-ն արդեն գրան�ված է" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "Չգրան�ված WebAuthn հավատարմագրի ID:" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "WebAuthn հավատարմագիրը չի պատկանում որևէ օգտվողի:" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "Չհաջողվե� հաստատել WebAuthn հավատարմագիրը� %(cause)s:" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ "Հավատարմագրերը գրան�ված չեն այս օգտագործման" #~ " համար (առաջին կամ երկրորդական)" #~ msgid "webauthn" #~ msgstr "webauthn" #~ msgid "WebAuthn Setup" #~ msgstr "WebAuthn կարգավորում" #~ msgid "Forgot password" #~ msgstr "Մոռա�ել եք գաղտնաբառը" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "Օգտագործել WebAuthn մուտք գործելու համար" #~ msgid "Sign in with WebAuthn" #~ msgstr "Մուտք գործել WebAuthn֊ի միջո�ով" #~ msgid "WebAuthn" #~ msgstr "WebAuthn" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "Այս հավելվածն օժանդակում է WebAuthn անվտանգության բանալիներին:" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "Վերանույնական�վելու համար օգտագործեք WebAuthn անվտանգության բանալին" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "Կարգավորեք նոր WebAuthn անվտանգության բանալի" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "�կսեք ձեր նոր անվտանգության բանալու համար եզակի անուն տրամադրելով." #~ msgid "Currently registered security keys:" #~ msgstr "Ներկայումս գրան�ված անվտանգության բանալիներ." #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "Ջնջել գոյություն ունե�ող WebAuthn անվտանգության բանալին" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "Մուտք գործեք� օգտագործելով WebAuthn անվտանգության բանալին" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "Օգտագործեք ձեր WebAuthn անվտանգության բանալին որպես երկրորդ գործոն" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider: (%(exerror)s -" #~ " %(exdesc)s). Please try again." #~ msgstr "" #~ msgid "Use Social Oauth to Sign In" #~ msgstr "Օգտագործել Social Oauth մուտք գործելու համար" #~ msgid "Sign in with %(provider)s" #~ msgstr "" #~ msgid "Username contains illegal characters" #~ msgstr "Օգտանունը պարունակում է անօրինական նիշեր" #~ msgid "Sign out" #~ msgstr "Դուրս գալ" #~ msgid "Click here to reset your password" #~ msgstr "�եր գաղտնաբառը վերականգնելու համար սեղմեք այստեղ" #~ msgid "Click the link below to reset your password:" #~ msgstr "�եր գաղտնաբառը վերականգնելու համար սեղմեք սեղմեք հղումին" #~ msgid "You can sign in to your account using the following code: %(token)s" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s." #~ msgstr "" pallets-eco-flask-security-72bf929/flask_security/translations/is_IS/000077500000000000000000000000001525721743500260245ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/is_IS/LC_MESSAGES/000077500000000000000000000000001525721743500276115ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/is_IS/LC_MESSAGES/flask_security.po000066400000000000000000001211611525721743500332020ustar00rootroot00000000000000# Icelandic (Iceland) translations for Flask-Security. # Copyright (C) 2022 ORGANIZATION # This file is distributed under the same license as the Flask-Security # project. # FIRST AUTHOR , 2022. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 4.0.0\n" "Report-Msgid-Bugs-To: jwag956@github.com\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: 2022-04-23 17:04+0000\n" "Last-Translator: \n" "Language: is_IS\n" "Language-Team: is_IS \n" "Plural-Forms: nplurals=2; plural=(n%10==1 && n%100!=11 ? 0 : 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:363 msgid "Login Required" msgstr "Innskráningar er þörf" #: flask_security/core.py:364 msgid "Welcome" msgstr "Velkomin(n)" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "Vinsamlegast staðfestu netfangið þitt" #: flask_security/core.py:366 msgid "Login instructions" msgstr "Leiðbeiningar fyrir innskráningu" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "Lykilorðið þitt hefur verið endurstillt" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "Lykilorðinu þínu hefur verið breytt" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "Leiðbeiningar um endurstillingu lykilorðs" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:371 msgid "Your requested username" msgstr "" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:423 msgid "Verification Code" msgstr "Staðfestingarkóði" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "Þú hefur ekki heimild til að skoða þessa auðlind." #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "Þakka þér fyrir. Netfangið þitt hefur verið staðfest." #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "Netfangið þitt hefur þegar verið staðfest." #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "Ógildur staðfestingarkóði." #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s er þegar í notkun á öðrum reikningi." #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:531 msgid "Password does not match" msgstr "Lykilorðið er ekki eins" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "Lykilorðin eru ekki eins" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "" "Leiðbeiningar um hvernig þú getur endurstillt lykilorðið þitt hafa verið " "sendar á %(email)s." #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "" #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "Netfangið þarfnast staðfestingar." #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "" #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "" #: flask_security/core.py:565 msgid "Invalid login token." msgstr "" #: flask_security/core.py:566 msgid "Account is disabled." msgstr "Aðgangurinn er óvirkur." #: flask_security/core.py:567 msgid "Email not provided" msgstr "Netfang var ekki gefið upp" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "Ógilt netfang" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "Ógildur kóði" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "" #: flask_security/core.py:578 msgid "Password not provided" msgstr "Lykilorð var ekki gefið upp" #: flask_security/core.py:580 #, python-format msgid "Password must be at least %(length)s characters" msgstr "Lykilorð verða að vera að minnsta kosti %(length)s stafir" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "Lykilorðið er ekki nógu flókið" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "Lykilorðið er á lista yfir brotin lykilorð" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "Gat ekki haft samband við síðu yfir brotin lykilorð" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "Símanúmerið er ekki gilt, t.d. vegna þess að landakóða vantar" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "" #: flask_security/core.py:591 msgid "Invalid password" msgstr "Ógilt lykilorð" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "Þér tókst að skrá þig inn." #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "Gleymt lykilorð?" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" "Þér tókst að endurstilla lykilorðið þitt og þú hefur verið skráð(ur) inn " "sjálfkrafa." #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "Nýja lykilorðið þitt verður að vera öðruvísi en gamla lykilorðið." #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "Þér tókst að breyta lykilorðinu þínu." #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "Vinsamlegast skráðu þig inn til að opna þessa síðu." #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "" #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "" #: flask_security/core.py:622 msgid "Code has been sent." msgstr "" #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "" #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" "Notendanafnið verður að vera að minnsta kosti %(min)d stafir að lengd og " "styttra en %(max)d stafir" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "Notendanafnið má eingöngu innihalda bókstafi og tölustafi" #: flask_security/core.py:667 msgid "Username not provided" msgstr "Notendanafn var ekki gefið upp" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" #: flask_security/forms.py:72 msgid "Change Method" msgstr "Breyta um aðferð" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Breyta lykilorði" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "" #: flask_security/forms.py:75 msgid "Delete" msgstr "Eyða" #: flask_security/forms.py:76 msgid "Email Address" msgstr "Netfang" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "Villur" #: flask_security/forms.py:79 msgid "Identity" msgstr "Auðkenni" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Innskráning" #: flask_security/forms.py:81 msgid "New Password" msgstr "Nýtt lykilorð" #: flask_security/forms.py:82 msgid "Passcode" msgstr "" #: flask_security/forms.py:83 msgid "Password" msgstr "Lykilorð" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "Símanúmer" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "Endurheimta lykilorð" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Skrá mig" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "Muna eftir mér" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "Endurstilla lykilorð" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "Endurtekið lykilorð" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "Senda aftur leiðbeiningar um staðfestingu" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "" #: flask_security/forms.py:93 msgid "Send Code" msgstr "Senda kóða" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Innskráning" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "" #: flask_security/forms.py:96 msgid "Submit" msgstr "Senda" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "Senda kóða" #: flask_security/forms.py:98 msgid "Username" msgstr "Notendanafn" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "Sannreyna lykilorð" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:105 msgid "authenticator" msgstr "" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "" #: flask_security/forms.py:108 msgid "SMS" msgstr "" #: flask_security/forms.py:109 msgid "password" msgstr "" #: flask_security/forms.py:110 msgid "passkey" msgstr "" #: flask_security/forms.py:111 msgid "none" msgstr "" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "Velja" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "Kóði eða lykilorð" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "Með tölvupósti" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "Með SMS skilaboði" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "Gælunafn" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "Notkun" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:241 msgid "Start" msgstr "Byrja" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Valmynd" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "Staðfesta reikning" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Senda leiðbeiningar fyrir endurstillingu lykilorðs" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Endurstilla lykilorð" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Kóði hefur verið sendur" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Lykilorðinu þínu hefur verið breytt." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Ef þú breyttir ekki lykilorðinu þínu," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "smelltu hér til að endurstilla það" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" "Ef þú breyttir ekki lykilorðinu þínu smelltu þá á hlekkinn til að " "endurstilla það." #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "" #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Velkomin(n) %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Skrá inn núna" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Það er ekkert lykilorð skráð fyrir þennan notanda" #~ msgid "Invalid Token" #~ msgstr "" #~ msgid "Your token has been confirmed" #~ msgstr "" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "" #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "" #~ "Þakka þér fyrir. Leiðbeiningar fyrir " #~ "staðfestingu hafa verið sendar á " #~ "%(email)s." #~ msgid "Two-factor Login" #~ msgstr "Tveggja þátta innskráning" #~ msgid "Two-factor Rescue" #~ msgstr "" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "" #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ msgid "Two factor authentication code" #~ msgstr "" #~ msgid "Two-factor Authentication" #~ msgstr "Tveggja-þátta auðkenning" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Breyta lykilorði" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Staðfesta reikninginn minn" #~ msgid "You can log into your account using the following code:" #~ msgstr "" #~ msgid "You can sign into your account using the following code:" #~ msgstr "" #~ msgid "Or use the link below:" #~ msgstr "" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Vinsamlegast staðfestu netfangið þitt á eftirfarandi slóð:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "Gleymt lykilorð" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider: (%(exerror)s -" #~ " %(exdesc)s). Please try again." #~ msgstr "" #~ msgid "Use Social Oauth to Sign In" #~ msgstr "" #~ msgid "Sign in with %(provider)s" #~ msgstr "" #~ msgid "Username contains illegal characters" #~ msgstr "Notendanafnið inniheldur óleyfileg tákn" #~ msgid "Sign out" #~ msgstr "Skrá út" #~ msgid "Click here to reset your password" #~ msgstr "Smelltu hér til að endurstilla lykilorðið þitt" #~ msgid "Click the link below to reset your password:" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s." #~ msgstr "" pallets-eco-flask-security-72bf929/flask_security/translations/it_IT/000077500000000000000000000000001525721743500260265ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/it_IT/LC_MESSAGES/000077500000000000000000000000001525721743500276135ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/it_IT/LC_MESSAGES/flask_security.po000066400000000000000000001254531525721743500332140ustar00rootroot00000000000000# Italian (Italy) translations for Flask-Security. # Copyright (C) 2026 THE AUTHORS # This file is distributed under the same license as the Flask-Security # project. # Giorgio Stampa <22690890+gissimo@users.noreply.github.com>, 2026. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 5.9.0\n" "Report-Msgid-Bugs-To: jwag956@github.com\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: 2026-08-29 14:14+0200\n" "Last-Translator: Giorgio Stampa " "<22690890+gissimo@users.noreply.github.com>\n" "Language: it_IT\n" "Language-Team: \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "Conferma il tuo nuovo indirizzo email" #: flask_security/core.py:363 msgid "Login Required" msgstr "Accesso richiesto" #: flask_security/core.py:364 msgid "Welcome" msgstr "Benvenuto/a" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "Conferma la tua email" #: flask_security/core.py:366 msgid "Login instructions" msgstr "Istruzioni per l'accesso" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "La tua password è stata reimpostata" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "La tua password è stata cambiata" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "Istruzioni per reimpostare la password" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "Il tuo nome utente è stato cambiato" #: flask_security/core.py:371 msgid "Your requested username" msgstr "Il tuo nome utente richiesto" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "Accesso a due fattori" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "Recupero dell'accesso a due fattori" #: flask_security/core.py:423 msgid "Verification Code" msgstr "Codice di verifica" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "Input non appropriato per l'API richiesta" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "Autenticazione fallita - identità o password/passcode non validi" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" "Se quell'indirizzo email è nel nostro sistema, riceverai un'email che " "descrive come reimpostare la tua password." #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "Se quell'identità è nel nostro sistema, ti è stato inviato un codice." #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "Non hai il permesso per visualizzare questa risorsa." #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "È necessario accedere per visualizzare questa risorsa." #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "È necessario riautenticarsi per accedere a questa risorsa" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" "Grazie. Per confermare il tuo indirizzo email %(email)s segui il link nel" " messaggio che ti abbiamo appena inviato." #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "Grazie. La tua email è stata confermata." #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "La tua email è già stata confermata." #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "Token di conferma non valido." #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s è già associata ad un account." #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" "L'attributo di identità '%(attr)s' con valore '%(value)s' è già associato" " ad un account." #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "Identità %(id)s non registrata" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" "Si è verificato un errore durante la comunicazione con il prestatore " "OAuth: (%(exerror)s - %(exdesc)s). Per favore prova di nuovo." #: flask_security/core.py:531 msgid "Password does not match" msgstr "La password non corrisponde" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "Le password non corrispondono" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "I reindirizzamenti all'esterno del dominio sono vietati" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "Codice di ripristino non valido" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "Non sono ancora stati generati codici di ripristino" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "Le istruzioni per reimpostare la password sono state inviate a %(email)s." #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "Non hai reimpostato la password entro %(within)s. " #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "Token di reimpostazione della password non valido." #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "L'email richiede conferma." #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Le istruzioni di conferma sono state inviate a %(email)s." #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "Non hai confermato la tua email entro %(within)s. " #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Non hai effettuato l'accesso entro %(within)s. Nuove istruzioni per " "l'accesso sono state inviate a %(email)s." #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Le istruzioni per l'accesso sono state inviate a %(email)s." #: flask_security/core.py:565 msgid "Invalid login token." msgstr "Token di accesso non valido." #: flask_security/core.py:566 msgid "Account is disabled." msgstr "L'account è disabilitato." #: flask_security/core.py:567 msgid "Email not provided" msgstr "Email non fornita" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "Indirizzo email non valido" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "Codice non valido" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "Il valore inserito non è valido per il campo" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "Il valore inserito deve essere composto da meno di %(length) caratteri" #: flask_security/core.py:578 msgid "Password not provided" msgstr "Password non fornita" #: flask_security/core.py:580 #, python-format msgid "Password must be at least %(length)s characters" msgstr "La password deve contenere almeno %(length)s caratteri" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "Password non abbastanza complessa" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "Password nell'elenco delle password violate" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "Impossibile contattare il sito delle password violate" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "" "Numero di telefono non valido, potrebbe mancare il corretto prefisso " "internazionale" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "L'utente specificato non esiste" #: flask_security/core.py:591 msgid "Invalid password" msgstr "Password non valida" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "La password o il codice inviato non sono validi" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "Hai effettuato l'accesso." #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "Password dimenticata?" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" "Hai reimpostato la tua password e l'accesso è stato effettuato " "automaticamente." #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" "Hai reimpostato la tua password. Autenticati utilizzando la nuova " "password." #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "La tua nuova password deve essere diversa dalla password precedente." #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "Hai cambiato la tua password." #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "Devi effettuare l'accesso per visualizzare questa pagina." #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "Devi riautenticarti per accedere a questa pagina." #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "Il token di aggiornamento non è valido (%(reason)s)" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "Riautenticazione riuscita" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "Puoi accedere a questa risorsa solo se non hai effettuato l'accesso." #: flask_security/core.py:622 msgid "Code has been sent." msgstr "Il codice è stato inviato." #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "Impossibile inviare il codice. Per favore prova di nuovo più tardi" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "Il tuo codice è stato confermato" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "Hai cambiato il tuo metodo a due fattori." #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "Al momento non disponi dei permessi per accedere a questa pagina" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "Il metodo selezionato non è valido" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "Hai disabilitato l'autorizzazione a due fattori." #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" "La configurazione deve essere completata entro %(within)s. Per favore " "ricomincia da capo." #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "Opzioni di accesso attualmente attive: %(method_list)s." #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "Il metodo richiesto non è valido" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "Configurazione dell'accesso unificato riuscita" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "Devi specificare un'identità valida per accedere" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "Usa questo codice per accedere: %(code)s" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "Hai cambiato il tuo nome utente" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" "Il nome utente deve essere composto da almeno %(min)d caratteri e meno di" " %(max)d caratteri" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "Il nome utente può contenere solo lettere e numeri" #: flask_security/core.py:667 msgid "Username not provided" msgstr "Nome utente non fornito" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "%(username)s è già associato ad un account." #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" "Le operazioni su passkey devono essere completate entro %(within)s. Per " "favore ricomincia." #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "Il nome per la nuova passkey è obbligatorio." #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "%(name)s è già associato ad una passkey." #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "%(name)s non registrato con l'utente attuale." #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" "I nomi delle passkey possono contenere solo lettere, numeri e una " "punteggiatura limitata." #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "Passkey con nome: %(name)s eliminata" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "Passkey con nome: %(name)s aggiunta" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "Passkey già registrata." #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "Passkey non registrata." #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "La passkey non appartiene a nessun utente." #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "Impossibile verificare la passkey: %(cause)s." #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "Passkey non registrata per questo uso (primario o secondario)" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "La credenziale dell'utente non corrisponde" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" "La conferma deve essere completata entro %(within)s. Per favore " "ricomincia." #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "Cambio di indirizzo email confermato" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" "Le istruzioni per confermare il tuo nuovo indirizzo email sono state " "inviate a %(email)s." #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "Se registrato, il nome utente verrà inviato al tuo indirizzo email." #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" "Configurazione tramite app di autenticazione (ad es. google, lastpass, " "authy)" #: flask_security/forms.py:72 msgid "Change Method" msgstr "Cambia metodo" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Cambia password" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "Codice di autenticazione" #: flask_security/forms.py:75 msgid "Delete" msgstr "Elimina" #: flask_security/forms.py:76 msgid "Email Address" msgstr "Indirizzo email" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "Configurazione tramite email" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "Errore(i)" #: flask_security/forms.py:79 msgid "Identity" msgstr "Identità" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Accedi" #: flask_security/forms.py:81 msgid "New Password" msgstr "Nuova password" #: flask_security/forms.py:82 msgid "Passcode" msgstr "Codice di accesso" #: flask_security/forms.py:83 msgid "Password" msgstr "Password" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "Numero di telefono" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "Recupera la password" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "Recupera il nome utente" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Registrati" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "Ricordati di me" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "Reimposta password" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "Ridigita password" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "Invia nuovamente istruzioni di conferma" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "Invia link di accesso" #: flask_security/forms.py:93 msgid "Send Code" msgstr "Invia codice" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Accedi" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "Configurazione tramite SMS" #: flask_security/forms.py:96 msgid "Submit" msgstr "Invia" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "Conferma codice" #: flask_security/forms.py:98 msgid "Username" msgstr "Nome utente" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "Verifica password" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "Google Authenticator" #: flask_security/forms.py:105 msgid "authenticator" msgstr "app di autenticazione" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "email" #: flask_security/forms.py:108 msgid "SMS" msgstr "SMS" #: flask_security/forms.py:109 msgid "password" msgstr "password" #: flask_security/forms.py:110 msgid "passkey" msgstr "passkey" #: flask_security/forms.py:111 msgid "none" msgstr "nessuno" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "Token di aggiornamento" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "Metodi disponibili" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "Disabilitare l'autenticazione a due fattori" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "Problemi di accesso al tuo account?/Dispositivo mobile smarrito?" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "Contatta l'amministratore" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "Mostra i codici di ripristino" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "Genera nuovi codici di ripristino" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "Codice di ripristino" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "Metodi di autenticazione a due fattori disponibili:" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "Seleziona" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "Invia codice tramite email" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "Usa codice di ripristino scaricato in precedenza" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "Codice o password" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "Tramite email" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "Tramite SMS" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "Imposta opzione di accesso aggiuntiva" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "Elimina l'opzione di accesso attiva" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "Nome" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "Utilizzo" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "Utilizzare come fattore di autenticazione primario" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "Utilizzare come fattore di autenticazione secondario" #: flask_security/webauthn.py:241 msgid "Start" msgstr "Inizia" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menù" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "Cambia email registrata" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "Cambia nome utente" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "Imposta autenticazione a due fattori" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "Imposta accesso unificato" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "Imposta passkey" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "Accesso unificato" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "Conferma account" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "Cambia email" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "Un messaggio di conferma sarà mandato a questo nuovo indirizzo email." #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "Al momento non hai una password: ne verrà aggiunta una." #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "Il nome utente attuale è: %(username)s" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Invia istruzioni per reimpostare la password" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "o" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "Usa una passkey per accedere" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "Accedi con una passkey" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "Usa l'accesso social per accedere" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "Continua con %(provider)s" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "Conferma disconnessione" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "Inserisci codice di ripristino" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "Codici di ripristino" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" "Assicurati di copiarli e conservarli in un luogo sicuro. Ogni codice può " "essere utilizzato una sola volta." #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "Genera nuovi codici di ripristino" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "Recupero del nome utente" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Reimposta password" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Invia nuovamente istruzioni di conferma" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "Seleziona metodo a due fattori" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" "L'autenticazione a due fattori aggiunge un ulteriore livello di sicurezza" " al tuo account" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "Oltre al nome utente e alla password, dovrai utilizzare un codice." #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "Metodo a due fattori attualmente attivo: %(method)s" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" "Apri un'app di autenticazione sul tuo dispositivo e scansiona il seguente" " codice QR (o inserisci manualmente il codice sottostante) per iniziare a" " ricevere i codici:" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "Codice di autenticazione a due fattori" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "Inserisci il codice per completare la configurazione" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "inserisci il codice numerico" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "Questa applicazione supporta l'impostazione dei codici di ripristino." #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "Si possono impostare qui." #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "Passkey" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "Questa applicazione supporta le passkey." #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "Autenticazione a due fattori" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "Inserisci il tuo codice di autenticazione generato tramite: %(method)s" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "Il codice per l'autenticazione è stato inviato al tuo indirizzo email" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "Ci è stata inviata un'email per reimpostare l'account dell'applicazione" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "Imposta accesso unificato" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Codice QR senza password" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "Nessun metodo è stato abilitato - niente da impostare" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "Inserisci qui il codice per completare la configurazione" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "Richiedere l'invio del codice monouso" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "Riautenticati" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Il codice è stato inviato" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "Usa una passkey per riautenticarti" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "Usa l'accesso social per riautenticarti" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "Imposta una nuova passkey" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "Inizia col fornire un nome univoco per la tua nuova passkey:" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "Passkey attualmente registrate:" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" "Nome: \"%s\" Utilizzo: \"%s\" Trasporti: \"%s\" Rilevabile: \"%s\" Tipo " "di dispositivo: \"%s\" Con backup? \"%s\" Ultimo utilizzo il: %s" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "Elimina una passkey esistente" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "Accedi con una passkey" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "Accedi con una passkey" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "Usa una passkey come secondo fattore" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "Riautenticati utilizzando una passkey" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" "Usa questo link per confermare il tuo nuovo " "indirizzo email." #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "Questo link scadrà entro %(within)s." #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "La tua email registrata attualmente è %(email)s." #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "Usa %(link)s per confermare il tuo nuovo indirizzo email." #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "La tua password è stata cambiata." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Se non hai cambiato tu la password," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "clicca qui per reimpostarla" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" "Se non hai cambiato tu la password, clicca sul link sottostante per " "reimpostarla." #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "Il tuo nome utente è stato cambiato." #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" "Usa questo link per confermare il " "tuo nuovo indirizzo email." #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "Usa %(confirmation_link)s per confermare il tuo nuovo indirizzo email." #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Benvenuto/a %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Puoi accedere al tuo account tramite il link sottostante:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Accedi adesso" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" "Usa questo link per reimpostare la tua " "password." #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "Usa %(reset_link)s per reimpostare la tua password." #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "La tua password è stata reimpostata." #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "Benvenuto/a %(username)s!" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "Puoi accedere al tuo account utilizzando il seguente codice: %(token)s" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "Questo codice scadrà entro %(within)s." #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "impossibile accedere all'account email" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "Puoi accedere al tuo account utilizzando il seguente codice: %(code)s" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "Oppure puoi usare questo link: Accedi" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "Questo codice/link scadrà entro %(within)s." #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "Oppure puoi usare questo link: %(login_link)s" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "Ciao," #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "Di recente hai chiesto di recuperare il tuo nome utente." #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "Il tuo nome utente è: %(username)s" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" "Se non hai avviato tu questa richiesta, puoi tranquillamente ignorare " "questo messaggio." #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "Ciao %(email)s!" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" "Qualcuno (tu?) ha provato a registrare questa email, che è già nel nostro" " sistema." #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "A questo account è associato anche il seguente nome utente: %(username)s." #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" "Puoi usare questo link per reimpostare la " "tua password." #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" "Non hai ancora confermato il tuo indirizzo email - usa questo link per farlo adesso." #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "A questo account è associato anche il seguente nome utente: %(username)s" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "Puoi usare questo link %(reset_link)s per reimpostare la tua password." #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" "Non hai ancora confermato il tuo indirizzo email - usa questo link: " "%(confirmation_link)s per farlo adesso." #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" "Hai tentato di registrarti con un nome utente \"%(username)s\" che è già " "associato ad un altro account." #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" "Per favore riavvia il processo di registrazione con un nome utente " "diverso." pallets-eco-flask-security-72bf929/flask_security/translations/ja_JP/000077500000000000000000000000001525721743500260015ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/ja_JP/LC_MESSAGES/000077500000000000000000000000001525721743500275665ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/ja_JP/LC_MESSAGES/flask_security.po000066400000000000000000001226621525721743500331660ustar00rootroot00000000000000# Japanese translations for Flask-Security. # Copyright (C) 2017 CERN # This file is distributed under the same license as the Flask-Security # project. # FIRST AUTHOR , 2017. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 2.0.1\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: 2018-01-25 14:12+0900\n" "Last-Translator: \n" "Language: ja\n" "Language-Team: \n" "Plural-Forms: nplurals=1; plural=0;\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:363 msgid "Login Required" msgstr "ログイン�必���" #: flask_security/core.py:364 msgid "Welcome" msgstr "よ���" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "メールアドレス�検証" #: flask_security/core.py:366 msgid "Login instructions" msgstr "ログイン手順" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "パスワード変更" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "パスワード�変更�れ���。" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "パスワード�設定手順" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:371 msgid "Your requested username" msgstr "" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:423 msgid "Verification Code" msgstr "" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "アクセス権��り��ん" #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "�り��������。メールアドレス�検証�れ���。" #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "メールアドレス�検証済���" #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "リンク�無効��" #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s �アカウント�既�作��れ����" #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:531 msgid "Password does not match" msgstr "パスワード�一致���ん" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "入力��パスワード�一致�����ん" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "ドメイン外��リダイレクト��止�れ����" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "パスワード��設定手順� %(email)s ��信�れ���" #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "リンク�無効��" #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "メールアドレス�検証�必���" #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "%(email)s�メールアドレス検証手順���信�れ���" #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "%(within)s以内�ログイン���ん���。ログイン手順を %(email)s ��度�信����。" #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "%(email)s�ログイン手順��信�れ���" #: flask_security/core.py:565 msgid "Invalid login token." msgstr "リンク�無効��" #: flask_security/core.py:566 msgid "Account is disabled." msgstr "アカウント�無効�������" #: flask_security/core.py:567 msgid "Email not provided" msgstr "メールアドレスを入力������" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "正��メールアドレスを入力������" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "" #: flask_security/core.py:578 msgid "Password not provided" msgstr "パスワードを入力������" #: flask_security/core.py:580 #, python-format msgid "Password must be at least %(length)s characters" msgstr "" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "入力を確�������" #: flask_security/core.py:591 msgid "Invalid password" msgstr "入力を確�������" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "ログイン����" #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "パスワードを忘れ�場�" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "パスワード��設定�完了����。" #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "新旧パスワード�����" #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "パスワード�変更�れ���" #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "ログイン������" #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "�度ログイン������" #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "" #: flask_security/core.py:622 msgid "Code has been sent." msgstr "" #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "" #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:667 msgid "Username not provided" msgstr "" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" #: flask_security/forms.py:72 msgid "Change Method" msgstr "" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "変更" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "" #: flask_security/forms.py:75 msgid "Delete" msgstr "" #: flask_security/forms.py:76 msgid "Email Address" msgstr "メールアドレス" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "" #: flask_security/forms.py:79 msgid "Identity" msgstr "" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "ログイン" #: flask_security/forms.py:81 msgid "New Password" msgstr "新��パスワード" #: flask_security/forms.py:82 msgid "Passcode" msgstr "" #: flask_security/forms.py:83 msgid "Password" msgstr "パスワード" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "�設定手順を�信" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "ユーザ登録" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "次回以�ログインを�略�る" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "パスワード変更" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "パスワード�入力" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "検証手順���信" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "ログイン手順を�信" #: flask_security/forms.py:93 msgid "Send Code" msgstr "" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "" #: flask_security/forms.py:96 msgid "Submit" msgstr "" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "" #: flask_security/forms.py:98 msgid "Username" msgstr "" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:105 msgid "authenticator" msgstr "" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "" #: flask_security/forms.py:108 msgid "SMS" msgstr "" #: flask_security/forms.py:109 msgid "password" msgstr "" #: flask_security/forms.py:110 msgid "passkey" msgstr "" #: flask_security/forms.py:111 msgid "none" msgstr "" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:241 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "メニュー" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "メールアドレス�検証" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "パスワード�設定手順��信" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "パスワード�設定" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "検証手順���信" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "パスワード�変更�れ���。" #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "パスワードを変更��覚����場����" #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "��リンクを開������。" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "" #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "よ��� %(email)s �" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "以下�リンク�よりログイン����。" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "ログイン" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" #~ msgid "You successfully confirmed password" #~ msgstr "" #~ msgid "Password confirmation is needed in order to access page" #~ msgstr "" #~ msgid "" #~ "Open your authenticator app on your " #~ "device and scan the following qrcode " #~ "to start receiving codes:" #~ msgstr "" #~ msgid "Or use the the link below:" #~ msgstr "" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ msgid "Please enter your authentication code" #~ msgstr "" #~ msgid "Setup Unified Sign In options" #~ msgstr "" #~ msgid "Please re-authenticate" #~ msgstr "�度ログイン������" #~ msgid "Please Enter Your Password" #~ msgstr "" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "パスワード�設定�れ����ん" #~ msgid "Invalid Token" #~ msgstr "" #~ msgid "Your token has been confirmed" #~ msgstr "" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "%(within)s以内�パスワードを設定���ん���。パスワード�設定手順を %(email)s ��度�信����。" #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "%(within)s以内�メールアドレス�検証�れ��ん���。新��検証手順を %(email)s ��信����。" #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "" #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "�登録�り��������。%(email)s�メールアドレス検証手順��信�れ���。" #~ msgid "Two-factor Login" #~ msgstr "" #~ msgid "Two-factor Rescue" #~ msgstr "" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "�度ログイン������" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "" #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ msgid "Two factor authentication code" #~ msgstr "" #~ msgid "Two-factor Authentication" #~ msgstr "" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "パスワード�変更" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "メールアドレス�検証" #~ msgid "You can log into your account using the following code:" #~ msgstr "" #~ msgid "You can sign into your account using the following code:" #~ msgstr "" #~ msgid "Or use the link below:" #~ msgstr "" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "以下�リンク�らメールアドレスを検証������:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "以下�リンク�よりメールアドレスを検証����。" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "パスワードを忘れ�場�" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider: (%(exerror)s -" #~ " %(exdesc)s). Please try again." #~ msgstr "" #~ msgid "Use Social Oauth to Sign In" #~ msgstr "" #~ msgid "Sign in with %(provider)s" #~ msgstr "" #~ msgid "Username contains illegal characters" #~ msgstr "" #~ msgid "Sign out" #~ msgstr "" #~ msgid "Click here to reset your password" #~ msgstr "パスワードを�設定�る�����リンクを開������。" #~ msgid "Click the link below to reset your password:" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s." #~ msgstr "" pallets-eco-flask-security-72bf929/flask_security/translations/nl_NL/000077500000000000000000000000001525721743500260205ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/nl_NL/LC_MESSAGES/000077500000000000000000000000001525721743500276055ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/nl_NL/LC_MESSAGES/flask_security.po000066400000000000000000001252361525721743500332050ustar00rootroot00000000000000# Dutch (Netherlands) translations for Flask-Security. # Copyright (C) 2017 CERN # This file is distributed under the same license as the Flask-Security # project. # FIRST AUTHOR , 2017. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 2.0.1\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: 2017-05-01 17:52+0200\n" "Last-Translator: FULL NAME \n" "Language: nl_NL\n" "Language-Team: nl_NL \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:363 msgid "Login Required" msgstr "Inloggen Verplicht" #: flask_security/core.py:364 msgid "Welcome" msgstr "Welkom" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "Gelieve uw e-mailadres te bevestigen" #: flask_security/core.py:366 msgid "Login instructions" msgstr "Aanmeld instructies" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "Uw wachtwoord werd gereset" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "Uw wachtwoord werd gewijzigd" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "Wachtwoord reset instructies" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:371 msgid "Your requested username" msgstr "" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:423 #, fuzzy msgid "Verification Code" msgstr "Authenticatie Code" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "U heeft niet de nodige rechten om deze pagina te zien." #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "Bedankt. Uw e-mailadres werd bevestigd." #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "Uw e-mailadres werd reeds bevestigd." #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "Ongeldige bevestiging token." #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s is al gelinkt aan een ander account." #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:531 msgid "Password does not match" msgstr "Wachtwoord komt niet overeen" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "Wachtwoorden komen niet overeen" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "Omleidingen buiten het domein zijn niet toegelaten" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "Instructies om uw wachtwoord te resetten werden verzonden naar %(email)s." #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "Ongeldig wachtwoord reset token." #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "E-mailadres moet bevestigd worden." #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "" "Instructies ter bevestiging van uw e-mailadres werden verzonden naar " "%(email)s." #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Je bent niet ingelogd geweest gedurende %(within)s. Nieuwe instructies om" " in te loggen werden verzonden naar%(email)s." #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Instructies om in te loggen werden verzonden naar %(email)s." #: flask_security/core.py:565 msgid "Invalid login token." msgstr "Ongeldige aanmelding." #: flask_security/core.py:566 msgid "Account is disabled." msgstr "Account is geblokkeerd." #: flask_security/core.py:567 msgid "Email not provided" msgstr "Email niet ingevuld" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "Ongeldig e-mailadres" #: flask_security/core.py:569 flask_security/core.py:624 #, fuzzy msgid "Invalid code" msgstr "Niet valide token" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "" #: flask_security/core.py:578 msgid "Password not provided" msgstr "Wachtwoord niet ingevuld" #: flask_security/core.py:580 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" msgstr "Uw wachtwoord moet minstens %(length)s karakters bevatten" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "Deze gebruiker bestaat niet" #: flask_security/core.py:591 msgid "Invalid password" msgstr "Ongeldig wachtwoord" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "U bent succesvol ingelogd." #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "Wachtwoord vergeten?" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "U heeft uw wachtwoord succesvol gereset en bent nu automatisch ingelogd." #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "Uw nieuw wachtwoord moet verschillend zijn van het voorgaande wachtwoord." #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "Uw wachtwoord werd met succes gewijzigd." #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "Gelieve in te loggen om deze pagina te zien." #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "Gelieve opnieuw in te loggen om deze pagina te zien." #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "" #: flask_security/core.py:622 msgid "Code has been sent." msgstr "" #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "U heeft succesvol uw Dubbele Authenticatie methode veranderd." #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "U heeft niet de juiste permissies om deze pagina te laden" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "De gemarkeerde methode is niet valide" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:647 #, fuzzy msgid "Requested method is not valid" msgstr "De gemarkeerde methode is niet valide" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:667 msgid "Username not provided" msgstr "" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" #: flask_security/forms.py:72 msgid "Change Method" msgstr "Verander Methode" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Verander wachtwoord" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "Authenticatie Code" #: flask_security/forms.py:75 msgid "Delete" msgstr "" #: flask_security/forms.py:76 msgid "Email Address" msgstr "E-mailadres" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "" #: flask_security/forms.py:79 msgid "Identity" msgstr "" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Aanmelden" #: flask_security/forms.py:81 msgid "New Password" msgstr "Nieuw wachtwoord" #: flask_security/forms.py:82 #, fuzzy msgid "Passcode" msgstr "wachtwoord" #: flask_security/forms.py:83 msgid "Password" msgstr "wachtwoord" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "Telefoonnummer" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "Herstel wachtwoord" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Registreer" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "Ingelogd blijven" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "reset wachtwoord" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "Type wachtwoord opnieuw" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "Verzend instructies om te bevestigen opnieuw" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "Verzend aanmeld link" #: flask_security/forms.py:93 msgid "Send Code" msgstr "" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "" #: flask_security/forms.py:96 msgid "Submit" msgstr "" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "" #: flask_security/forms.py:98 msgid "Username" msgstr "" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "Wachtwoord Verificatie" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:105 msgid "authenticator" msgstr "" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "" #: flask_security/forms.py:108 msgid "SMS" msgstr "" #: flask_security/forms.py:109 msgid "password" msgstr "" #: flask_security/forms.py:110 msgid "passkey" msgstr "" #: flask_security/forms.py:111 msgid "none" msgstr "" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:241 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menu" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "Bevestig account" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Verzend wachtwoord reset instructies" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Reset wachtwoord" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Verzend bevestiging instructies opnieuw" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "The code voor authenticatie is naar uw e-mail adres verzonden" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Uw wachtwoord werd gewijzigd." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Als u uw wachtwoord niet hebt aangepast," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "Klik hier om het te resetten" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "" #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Welkom, %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "U kunt inloggen door onderstaande link te gebruiken:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Nu inloggen" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "kan niet in het e-mail account" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" #~ msgid "You successfully confirmed password" #~ msgstr "U heeft succesvol uw wachtwoord aangepast" #~ msgid "Password confirmation is needed in order to access page" #~ msgstr "Wachtwoord bevestiging is nodig voor we deze pagina kunnen laten zien" #~ msgid "" #~ "Open your authenticator app on your " #~ "device and scan the following qrcode " #~ "to start receiving codes:" #~ msgstr "" #~ "Open Google Authenticator op uw toestel" #~ " en scan de volgende qrcode om " #~ "codes te kunnen ontvangen:" #~ msgid "Or use the the link below:" #~ msgstr "" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ "Naast uw gebruikersnaam en wachtwoord, " #~ "heeft u ook een code nodig dat " #~ "we u zullen toezenden" #~ msgid "Please enter your authentication code" #~ msgstr "Voer uw authenticatie code in" #~ msgid "Setup Unified Sign In options" #~ msgstr "" #~ msgid "Please re-authenticate" #~ msgstr "" #~ msgid "Please Enter Your Password" #~ msgstr "Voer uw wachtwoord in" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Er is geen wachtwoord gezet voor deze gebruiker" #~ msgid "Invalid Token" #~ msgstr "Niet valide token" #~ msgid "Your token has been confirmed" #~ msgstr "Uw token is bevestigd" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "U heeft uw wachtwoord niet gereset " #~ "gedurende %(within)s. Nieuwe instructies " #~ "werden verzonden naar %(email)s." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "U heeft uw e-mailadres niet bevestigd" #~ " in de voorziene %(within)s. Nieuwe " #~ "instructies ter bevestiging van uw " #~ "e-mailadres werden verzonden naar %(email)s." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "U bent niet aangemeld. Voer alstublieft de juiste gegevens in." #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ "Om verder in te loggen moet U " #~ "de code die we naar uw e-mail " #~ "hebben gezonden invoeren" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "Naar welk telefoonnummer kunnen we code verzenden?" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ "Een bericht is naar uw e-mail " #~ "adres verzonden om uw account te " #~ "herstellen" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "Bedankt. Instructies voor bevestiging zijn verzonden naar %(email)s." #~ msgid "Two-factor Login" #~ msgstr "Dubbele Authenticatie Aanmelding" #~ msgid "Two-factor Rescue" #~ msgstr "Dubbele Authenticatie Herstellen" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Gelieve opnieuw in te loggen om deze pagina te zien." #~ msgid "You successfully disabled two factor authorization." #~ msgstr "U heeft succesvol Dubbele Authenticatie uitgeschakeld." #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ "Dubbele Authenticatie voegt een extra " #~ "laag van beveiliging toe aan uw " #~ "account" #~ msgid "Two factor authentication code" #~ msgstr "Dubbele Authenticatie code" #~ msgid "Two-factor Authentication" #~ msgstr "Dubbele Authenticatie" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Verander wachtwoord" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Bevestig mijn account" #~ msgid "You can log into your account using the following code:" #~ msgstr "U kunt inloggen door de volgende code te gebruiken:" #~ msgid "You can sign into your account using the following code:" #~ msgstr "U kunt inloggen door de volgende code te gebruiken:" #~ msgid "Or use the link below:" #~ msgstr "" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Gelieve uw e-mailadres te bevestigen via onderstaande link:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "U kan uw e-mailadres bevestigen via de onderstaande link:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "Wachtwoord vergeten" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider: (%(exerror)s -" #~ " %(exdesc)s). Please try again." #~ msgstr "" #~ msgid "Use Social Oauth to Sign In" #~ msgstr "" #~ msgid "Sign in with %(provider)s" #~ msgstr "" #~ msgid "Username contains illegal characters" #~ msgstr "" #~ msgid "Sign out" #~ msgstr "" #~ msgid "Click here to reset your password" #~ msgstr "Klik hier om uw wachtwoord te resetten" #~ msgid "Click the link below to reset your password:" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s." #~ msgstr "" pallets-eco-flask-security-72bf929/flask_security/translations/pl_PL/000077500000000000000000000000001525721743500260245ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/pl_PL/LC_MESSAGES/000077500000000000000000000000001525721743500276115ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/pl_PL/LC_MESSAGES/flask_security.po000066400000000000000000001303051525721743500332020ustar00rootroot00000000000000# Polish translation for Flask-Security # Copyright (C) 2020 Kamil Daniewski # This file is distributed under the same license as the Flask-Security # project. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 2.0.1\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: 2020-11-28 10:19+0100\n" "Last-Translator: Kamil Daniewski \n" "Language: pl_PL\n" "Language-Team: pl_PL \n" "Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && " "(n%100<10 || n%100>=20) ? 1 : 2);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:363 msgid "Login Required" msgstr "Logowanie jest wymagane" #: flask_security/core.py:364 msgid "Welcome" msgstr "Witamy" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "Prosimy o potwierdzenie Twojego adresu e-mail" #: flask_security/core.py:366 msgid "Login instructions" msgstr "Instrukcje logowania" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "Twoje hasło zostało zresetowane" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "Twoje hasło zostało zmienione" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "Instrukcje zmiany hasła" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:371 msgid "Your requested username" msgstr "" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:423 msgid "Verification Code" msgstr "Kod weryfikacyjny" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "Nieprawidłowe dane dla żądanego API" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "Nie posiadasz uprawnień, aby wyświetlić tę stronę." #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "Dziękujemy. Twój adres e-mail został potwierdzony." #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "Twój adres e-mail już został potwierdzony." #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "Nieprawidłowy token potwierdzania adresu e-mail." #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s jest już powiązany z kontem." #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" "Atrybut identyfikujący '%(attr)s' z wartością '%(value)s' jest już " "powiązany z kontem." #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:531 msgid "Password does not match" msgstr "Hasło nie pasuje" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "Hasła nie pasują do siebie" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "Przekierowania poza domenę są zabronione" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "Instrukcje resetowania hasła zostały wysłane na adres %(email)s." #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "Nieprawidłowy token resetowania hasła." #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "Wymagane jest potwierdzenie adresu e-mail." #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Instrukcje potwierdzenia adresu e-mail zostały wysłane na adres %(email)s." #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Nie zalogowałeś się w ciągu %(within)s. Nowe instrukcje logowania zostały" " wysłane na adres %(email)s." #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Instrukcje logowania zostały wysłane na adres %(email)s." #: flask_security/core.py:565 msgid "Invalid login token." msgstr "Nieprawidłowy token logowania." #: flask_security/core.py:566 msgid "Account is disabled." msgstr "Konto jest wyłączone." #: flask_security/core.py:567 msgid "Email not provided" msgstr "Adres e-mail nie został wprowadzony" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "Nieprawidłowy adres e-mail" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "Nieprawidłowy kod" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "" #: flask_security/core.py:578 msgid "Password not provided" msgstr "Hasło nie zostało wprowadzone" #: flask_security/core.py:580 #, python-format msgid "Password must be at least %(length)s characters" msgstr "Hasło musi zawierać co najmniej %(length)s znaków" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "Hasło nie jest wystarczająco złożone" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "Hasło znajduje się na liście haseł wykradzionych" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "" "Nie udało się dotrzeć do podmiotu sprawdzającego hasło w bazie " "wykradzionych haseł" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "Nieprawidłowiy numer telefonu (upewnij się, że zawiera kod kraju)" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "Ten użytkownik nie istnieje" #: flask_security/core.py:591 msgid "Invalid password" msgstr "Nieprawidłowe hasło" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "Hasło lub wprowadzony kod są nieprawidłowe" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "Zostałeś zalogowany pomyślnie." #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "Zapomniałeś hasło?" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "Ustawiono nowe hasło i zostałeś zalogowany pomyślnie." #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "Twoje nowe hasło musi być inne, niż obecne hasło." #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "Pomyślnie zmieniłeś hasło." #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "Prosimy o zalogowanie się, aby móc odwiedzić tę stronę." #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "Prosimy o ponowne zalogowanie się, aby móc odwiedzić tę stronę." #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "Ponownie zalogowano" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "Możesz odwiedzić tę stronę tylko będąc niezalogowanym." #: flask_security/core.py:622 msgid "Code has been sent." msgstr "" #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "Nie udało się wysłać kodu. Prosimy spróbować później" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "Metoda logowania dwuskładnikowego została zmieniona pomyślnie." #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "Nie posiadasz uprawnień, aby odwiedzić tę stronę" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "Wybrana metoda jest niewłaściwa" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" "Ustawienie musi zostać ukończone w ciągu %(within)s. Prosimy zacząć " "ponownie." #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "Żądana metoda jest niewłaściwa" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "Ujednolicone logowanie przebiegło pomyślnie" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "Musisz ustawić prawidłowy identyfikator, aby się zalogować" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:667 msgid "Username not provided" msgstr "" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" "Ustaw przy pomocy zewnętrznej aplikacji uwierzytelniania (np. Google, " "Lastpass, Authy)" #: flask_security/forms.py:72 msgid "Change Method" msgstr "Zmień metodę" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Zmień hasło" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "Kod uwierzytelniania" #: flask_security/forms.py:75 msgid "Delete" msgstr "" #: flask_security/forms.py:76 msgid "Email Address" msgstr "Adres e-mail" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "Ustaw przy pomocy adresu e-mail" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "Błędy" #: flask_security/forms.py:79 msgid "Identity" msgstr "Identyfikator" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Zaloguj" #: flask_security/forms.py:81 msgid "New Password" msgstr "Nowe hasło" #: flask_security/forms.py:82 msgid "Passcode" msgstr "Kod dostępu" #: flask_security/forms.py:83 msgid "Password" msgstr "Hasło" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "Numer telefonu" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "Odzyskaj hasło" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Zarejestruj" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "Zapamiętaj mnie" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "Zresetuj hasło" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "Przepisz hasło" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "Ponownie wyślij instrukcje potwierdzania adresu e-mail" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "Wyślij link logowania" #: flask_security/forms.py:93 msgid "Send Code" msgstr "Wyślij kod" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Zaloguj" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "Ustaw przy pomocy wiadomości SMS" #: flask_security/forms.py:96 msgid "Submit" msgstr "Wyślij" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "Kod zatwierdzenia" #: flask_security/forms.py:98 msgid "Username" msgstr "" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "Potwierdź hasło" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:105 msgid "authenticator" msgstr "" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "" #: flask_security/forms.py:108 msgid "SMS" msgstr "" #: flask_security/forms.py:109 msgid "password" msgstr "" #: flask_security/forms.py:110 msgid "passkey" msgstr "" #: flask_security/forms.py:111 msgid "none" msgstr "" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "Dostępne metody" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "Kod lub hasło" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "Poprzez adres e-mail" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "Poprzez wiadomość SMS" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:241 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menu" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "Logowanie ujednolicone" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "Potwierdź konto" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Wyślij instrukcje resetowania hasła" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Resetuj hasło" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Ponownie wyślij instrukcje potwierdzania rejestracji" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "Kod uwierzytelniania został do Ciebie wysłany na adres e-mail" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Bezhasłowy kod QR" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "Żadna z metod nie została włączona" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "Zażądaj jednorazowego wysłania kodu" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Kod został wysłany" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Twoje hasło zostało zmienione." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Jeśli nie zmieniłeś swojego hasła," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "kliknij tutaj, aby je zresetować" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" "Jeśli nie zmieniłeś swojego hasła, kliknij w poniższy link, aby je " "zresetować." #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "" #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Witamy %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Możesz logować się na swoje konto poprzez poniższy link:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Zaloguj teraz" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "brak dostępu do konta mailowego" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" #~ msgid "" #~ "Open your authenticator app on your " #~ "device and scan the following qrcode " #~ "to start receiving codes:" #~ msgstr "" #~ "Otwórz Twoją aplikację uwierzytelniania na " #~ "swoim urządzeniu i zeskanuj poniższy kod" #~ " QR, aby móc otrzymywać kolejne kody:" #~ msgid "Or use the the link below:" #~ msgstr "Lub używając poniższego linku:" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ "Oprócz Twojej nazwy użytkownika i hasła," #~ " będziesz musiał jeszcze użyć kodu, " #~ "który od nas otrzymasz" #~ msgid "Please enter your authentication code" #~ msgstr "Prosimy o wprowadzenie Twojego kodu uwierzytelniania" #~ msgid "Setup Unified Sign In options" #~ msgstr "Ustaw opcje logowania ujednoliconego" #~ msgid "Please re-authenticate" #~ msgstr "Prosimy o ponowne zalogowanie" #~ msgid "Please Enter Your Password" #~ msgstr "Prosimy o wprowadzenie hasła" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Hasło nie zostało ustawione przez tego użytkownika" #~ msgid "Invalid Token" #~ msgstr "Nieprawidłowy token" #~ msgid "Your token has been confirmed" #~ msgstr "Twój token nie został potwierdzony" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "Nie ustawiłeś hasła w ciągu %(within)s." #~ " Nowe instrukcje zostały wysłane na " #~ "adres %(email)s." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "Nie potwierdziłeś adresu e-mail w ciągu" #~ " %(within)s. Nowe instrukcje zostały " #~ "wysłane na adres %(email)s." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "" #~ "Nie jesteś zalogowany. Prosimy o " #~ "przesłanie prawidłowych danych uwierzytelniania." #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ "Aby dokończyć proces logowania, prosimy " #~ "wprowadzić kod, który został wysłany na" #~ " Twój adres e-mail" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "Na jaki numer telefonu powinien zostać wysłany kod?" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ "Wiadomość e-mail została do nas wysłana" #~ " w celu zresetowania Twojego konta " #~ "aplikacji" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "" #~ "Dziękujemy. Instrukcje potwierdzenia rejestracji " #~ "zostały wysłane na adres %(email)s." #~ msgid "Two-factor Login" #~ msgstr "Logowanie dwuskładnikowe" #~ msgid "Two-factor Rescue" #~ msgstr "Pomoc w logowaniu dwuskładnikowym" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Musisz zalogować się ponownie, aby wyświetlić tę stronę" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "Pomyślnie wyłączyłeś logowanie dwuskładnikowe." #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ "Uwierzytelnianie dwuskładnikowe jest dodatkową " #~ "warstwą bezpieczeństwa dla Twojego konta" #~ msgid "Two factor authentication code" #~ msgstr "Kod uwierzytelniania dwuskładnikowego" #~ msgid "Two-factor Authentication" #~ msgstr "Uwierzytelnianie dwuskładnikowe" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Zmień hasło" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Potwierdź moje konto" #~ msgid "You can log into your account using the following code:" #~ msgstr "Możesz logować się na swoje konto używając poniższego kodu:" #~ msgid "You can sign into your account using the following code:" #~ msgstr "Możesz logować się na swoje konto używając poniższego kodu:" #~ msgid "Or use the link below:" #~ msgstr "Lub używając poniższego linku:" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Prosimy o potwierdzenie Twojego adresu e-mail poprzez poniższy link:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Możesz potwierdzić swój adres e-mail poprzez poniższy link:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "Użyj tego kodu, aby się zalogować: %(code)s." #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "Zapomniałem hasło" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider: (%(exerror)s -" #~ " %(exdesc)s). Please try again." #~ msgstr "" #~ msgid "Use Social Oauth to Sign In" #~ msgstr "" #~ msgid "Sign in with %(provider)s" #~ msgstr "" #~ msgid "Username contains illegal characters" #~ msgstr "" #~ msgid "Sign out" #~ msgstr "" #~ msgid "Click here to reset your password" #~ msgstr "Kliknij tutaj, aby zresetować swoje hasło" #~ msgid "Click the link below to reset your password:" #~ msgstr "Kliknij na poniższy link, aby zresetować swoje hasło:" #~ msgid "You can sign in to your account using the following code: %(token)s" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s." #~ msgstr "" pallets-eco-flask-security-72bf929/flask_security/translations/pt_BR/000077500000000000000000000000001525721743500260245ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/pt_BR/LC_MESSAGES/000077500000000000000000000000001525721743500276115ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/pt_BR/LC_MESSAGES/flask_security.po000066400000000000000000001217771525721743500332170ustar00rootroot00000000000000# Portuguese (Brazil) translations for Flask-Security. # Copyright (C) 2017 CERN # This file is distributed under the same license as the Flask-Security # project. # José Neto , 2017. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 2.0.1\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: 2017-09-27 23:39-0300\n" "Last-Translator: José Neto \n" "Language: pt_BR\n" "Language-Team: \n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:363 msgid "Login Required" msgstr "Login obrigatório" #: flask_security/core.py:364 msgid "Welcome" msgstr "Bem-vindo" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "Por favor, confirme seu email" #: flask_security/core.py:366 msgid "Login instructions" msgstr "Instruções de login" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "Sua senha foi redefinida" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "Sua senha foi alterada" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "Instruções para redfinir a senha" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:371 msgid "Your requested username" msgstr "" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:423 msgid "Verification Code" msgstr "" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "Você não tem permissão para ver este recurso" #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "Obrigado. Seu email foi confirmado." #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "Seu email já foi confirmado." #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "Token de confirmação inválido." #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s já está associado a uma conta." #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:531 msgid "Password does not match" msgstr "Senha não confere" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "Senhas não conferem" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "Redirecionamentos para fora do domínio são proibidos" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "As instruções para redefinir sua senha foram enviadas para %(email)s." #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "Token de redefinição de senha inválido." #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "O email requer confirmação." #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "As instruções de confirmaç foram enviadas para %(email)s." #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Você não logou dentro de %(within)s. Novas instruções para logar foram " "enviadas para %(email)s." #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Instruções para logar foram enviadas para %(email)s." #: flask_security/core.py:565 msgid "Invalid login token." msgstr "Token de login inválido." #: flask_security/core.py:566 msgid "Account is disabled." msgstr "Conta desabilitada." #: flask_security/core.py:567 msgid "Email not provided" msgstr "Email não informado" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "Endereço de email inválido" #: flask_security/core.py:569 flask_security/core.py:624 #, fuzzy msgid "Invalid code" msgstr "Senha inválida" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "" #: flask_security/core.py:578 msgid "Password not provided" msgstr "Senha não informada" #: flask_security/core.py:580 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" msgstr "A senha deve ter pelo menos 6 caracteres" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "Usuário não existe" #: flask_security/core.py:591 msgid "Invalid password" msgstr "Senha inválida" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "Você logou com sucesso." #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "Esqueceu a senha?" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "Você redefiniu sua senha com sucesso e foi logado automaticamente." #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "Sua nova senha deve ser diferente da sua senha anterior." #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "Você alterou sua senha com sucesso." #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "Por favor, logue para acessar esta página." #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "Por favor, reautentique-se para acessar esta página." #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "" #: flask_security/core.py:622 msgid "Code has been sent." msgstr "" #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "" #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:667 msgid "Username not provided" msgstr "" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" #: flask_security/forms.py:72 msgid "Change Method" msgstr "" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Alterar senha" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "" #: flask_security/forms.py:75 msgid "Delete" msgstr "" #: flask_security/forms.py:76 msgid "Email Address" msgstr "Endereço de email" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "" #: flask_security/forms.py:79 msgid "Identity" msgstr "" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Login" #: flask_security/forms.py:81 msgid "New Password" msgstr "Nova senha" #: flask_security/forms.py:82 msgid "Passcode" msgstr "" #: flask_security/forms.py:83 msgid "Password" msgstr "Senha" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "Recuperar senha" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Registro" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "Lembre de mim" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "Redefinir senha" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "Reescreva a senha" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "Reenviar instruções de confirmação" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "Enviar link de login" #: flask_security/forms.py:93 msgid "Send Code" msgstr "" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "" #: flask_security/forms.py:96 msgid "Submit" msgstr "" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "" #: flask_security/forms.py:98 msgid "Username" msgstr "" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:105 msgid "authenticator" msgstr "" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "" #: flask_security/forms.py:108 msgid "SMS" msgstr "" #: flask_security/forms.py:109 msgid "password" msgstr "" #: flask_security/forms.py:110 msgid "passkey" msgstr "" #: flask_security/forms.py:111 msgid "none" msgstr "" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:241 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menu" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "Confirmar conta" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Enviar instruções para redefinir a senha" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Redefinir senha" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Reenviar instruções de confirmação" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Sua senha foi alterada." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Se você não alterou sua senha," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "clique aqui para resetar" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "" #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Bem-vindo %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Você pode logar na sua conta através do link abaixo:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Logar agora" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" #~ msgid "You successfully confirmed password" #~ msgstr "" #~ msgid "Password confirmation is needed in order to access page" #~ msgstr "" #~ msgid "" #~ "Open your authenticator app on your " #~ "device and scan the following qrcode " #~ "to start receiving codes:" #~ msgstr "" #~ msgid "Or use the the link below:" #~ msgstr "" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ msgid "Please enter your authentication code" #~ msgstr "" #~ msgid "Setup Unified Sign In options" #~ msgstr "" #~ msgid "Please re-authenticate" #~ msgstr "Por favor, reautentique-se para acessar esta página." #~ msgid "Please Enter Your Password" #~ msgstr "" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Nenhuma senha definida para este usuário" #~ msgid "Invalid Token" #~ msgstr "" #~ msgid "Your token has been confirmed" #~ msgstr "" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "Você não redefiniu sua senha dentro " #~ "de %(within)s. Novas instruções foram " #~ "enviadas para %(email)s." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "Você não confirmou seu email dentro " #~ "de %(within)s. Novas instruções foram " #~ "enviadas para %(email)s." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "" #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "" #~ "Obrigado. As instruções para a " #~ "confirmação foram enviadas para %(email)s." #~ msgid "Two-factor Login" #~ msgstr "" #~ msgid "Two-factor Rescue" #~ msgstr "" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Por favor, reautentique-se para acessar esta página." #~ msgid "You successfully disabled two factor authorization." #~ msgstr "" #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ msgid "Two factor authentication code" #~ msgstr "" #~ msgid "Two-factor Authentication" #~ msgstr "" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Alterar senha" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Confirmar minha conta" #~ msgid "You can log into your account using the following code:" #~ msgstr "" #~ msgid "You can sign into your account using the following code:" #~ msgstr "" #~ msgid "Or use the link below:" #~ msgstr "" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Por favor, confirme seu email através do link abaixo:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Você pode confirmar seu email através do link abaixo:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "Esqueceu a senha" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider: (%(exerror)s -" #~ " %(exdesc)s). Please try again." #~ msgstr "" #~ msgid "Use Social Oauth to Sign In" #~ msgstr "" #~ msgid "Sign in with %(provider)s" #~ msgstr "" #~ msgid "Username contains illegal characters" #~ msgstr "" #~ msgid "Sign out" #~ msgstr "" #~ msgid "Click here to reset your password" #~ msgstr "Clique aqui para redefinir sua senha" #~ msgid "Click the link below to reset your password:" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s." #~ msgstr "" pallets-eco-flask-security-72bf929/flask_security/translations/pt_PT/000077500000000000000000000000001525721743500260445ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/pt_PT/LC_MESSAGES/000077500000000000000000000000001525721743500276315ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/pt_PT/LC_MESSAGES/flask_security.po000066400000000000000000001224451525721743500332300ustar00rootroot00000000000000# Portuguese (Portugal) translations for Flask-Security. # Copyright (C) 2017 CERN # This file is distributed under the same license as the Flask-Security # project. # Micael Grilo , 2018. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 2.0.1\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: 2018-04-27 14:00+0100\n" "Last-Translator: Micael Grilo \n" "Language: pt_PT\n" "Language-Team: \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:363 msgid "Login Required" msgstr "Login obrigatório" #: flask_security/core.py:364 msgid "Welcome" msgstr "Bem-vindo" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "Por favor, confirme o seu email" #: flask_security/core.py:366 msgid "Login instructions" msgstr "Instruções de login" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "A sua palavra-passe foi redefinida" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "A sua palavra-passe foi alterada" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "Instruções para redefinir a palavra-passe" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:371 msgid "Your requested username" msgstr "" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:423 msgid "Verification Code" msgstr "" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "Não tem permissões para ver este recurso" #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "Obrigado. O seu email foi confirmado." #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "O seu email já foi confirmado." #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "Token de confirmação inválido." #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s já está associado a uma conta." #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:531 msgid "Password does not match" msgstr "Palavra-passe não coincide" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "Palavras-passe não coincidem" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "Redirecionamentos para fora do domínio são proibidos" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "" "As instruções para redefinir a sua palavra-passe foram enviadas para " "%(email)s." #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "Token de redefinição de senha inválido." #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "O email requer confirmação." #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "As instruções de confirmação foram enviadas para %(email)s." #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Não iniciou sessão dentro de %(within)s. Novas instruções de inicio de " "sessão foram enviadas para %(email)s." #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Instruções para o inicio de sessão foram enviadas para %(email)s." #: flask_security/core.py:565 msgid "Invalid login token." msgstr "Token de login inválido." #: flask_security/core.py:566 msgid "Account is disabled." msgstr "Conta desactivada." #: flask_security/core.py:567 msgid "Email not provided" msgstr "Email em falta" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "Endereço de email inválido" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "" #: flask_security/core.py:578 msgid "Password not provided" msgstr "Palavra-passe em falta" #: flask_security/core.py:580 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" msgstr "A palavra-passe deve ter pelo menos %(length)s caracteres" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "Utilizador não existe" #: flask_security/core.py:591 msgid "Invalid password" msgstr "Palavra-passe inválida" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "Sessão iniciada com sucesso." #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "Esqueceu a palavra-passe?" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" "Redefiniu a sua palavra-passe com sucesso e iniciou sessão " "automaticamente." #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "A sua nova palavra-passe deve ser diferente da anterior." #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "Alterou a sua palavra-passe com sucesso." #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "Por favor, inicie sessão para aceder a esta página." #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "Por favor, reautentique-se para aceder esta página." #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "" #: flask_security/core.py:622 msgid "Code has been sent." msgstr "" #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "" #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:667 msgid "Username not provided" msgstr "" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" #: flask_security/forms.py:72 msgid "Change Method" msgstr "" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Alterar palavra-passe" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "" #: flask_security/forms.py:75 msgid "Delete" msgstr "" #: flask_security/forms.py:76 msgid "Email Address" msgstr "Endereço de email" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "" #: flask_security/forms.py:79 msgid "Identity" msgstr "" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Login" #: flask_security/forms.py:81 msgid "New Password" msgstr "Nova palavra-passe" #: flask_security/forms.py:82 msgid "Passcode" msgstr "" #: flask_security/forms.py:83 msgid "Password" msgstr "Palavra-passe" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "Recuperar palavra-passe" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Registo" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "Lembrar-me" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "Redefinir palavra-passe" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "Reescreva a palavra-passe" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "Reenviar instruções de confirmação" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "Enviar endereço de login" #: flask_security/forms.py:93 msgid "Send Code" msgstr "" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "" #: flask_security/forms.py:96 msgid "Submit" msgstr "" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "" #: flask_security/forms.py:98 msgid "Username" msgstr "" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:105 msgid "authenticator" msgstr "" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "" #: flask_security/forms.py:108 msgid "SMS" msgstr "" #: flask_security/forms.py:109 msgid "password" msgstr "" #: flask_security/forms.py:110 msgid "passkey" msgstr "" #: flask_security/forms.py:111 msgid "none" msgstr "" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:241 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menu" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "Confirmar conta" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Enviar instruções para redefinir a palavra-passe" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Redefinir palavra-passe" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Reenviar instruções de confirmação" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "A sua palavra-passe foi alterada." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Não alterou a sua palavra-passe," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "clique aqui para redefinir" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "" #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Bem-vindo %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Você pode iniciar sessão na sua conta através do endereço abaixo:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Iniciar sessão" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" #~ msgid "You successfully confirmed password" #~ msgstr "" #~ msgid "Password confirmation is needed in order to access page" #~ msgstr "" #~ msgid "" #~ "Open your authenticator app on your " #~ "device and scan the following qrcode " #~ "to start receiving codes:" #~ msgstr "" #~ msgid "Or use the the link below:" #~ msgstr "" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ msgid "Please enter your authentication code" #~ msgstr "" #~ msgid "Setup Unified Sign In options" #~ msgstr "" #~ msgid "Please re-authenticate" #~ msgstr "Por favor, reautentique-se para aceder esta página." #~ msgid "Please Enter Your Password" #~ msgstr "" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Nenhuma palavra-passe foi definida para este utilizador" #~ msgid "Invalid Token" #~ msgstr "" #~ msgid "Your token has been confirmed" #~ msgstr "" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "Não redefiniu a sua palavra-passe " #~ "dentro de %(within)s. Novas instruções " #~ "foram enviadas para %(email)s." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "Não confirmou o seu email dentro " #~ "de %(within)s. Novas instruções foram " #~ "enviadas para %(email)s." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "" #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "" #~ "Obrigado. As instruções para a " #~ "confirmação foram enviadas para %(email)s." #~ msgid "Two-factor Login" #~ msgstr "" #~ msgid "Two-factor Rescue" #~ msgstr "" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Por favor, reautentique-se para aceder esta página." #~ msgid "You successfully disabled two factor authorization." #~ msgstr "" #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ msgid "Two factor authentication code" #~ msgstr "" #~ msgid "Two-factor Authentication" #~ msgstr "" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Alterar palavra-passe" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Confirmar minha conta" #~ msgid "You can log into your account using the following code:" #~ msgstr "" #~ msgid "You can sign into your account using the following code:" #~ msgstr "" #~ msgid "Or use the link below:" #~ msgstr "" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Por favor, confirme o seu email através do endereço abaixo:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Você pode confirmar o seu email através do endereço abaixo:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "Esqueceu a palavra-passe" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider: (%(exerror)s -" #~ " %(exdesc)s). Please try again." #~ msgstr "" #~ msgid "Use Social Oauth to Sign In" #~ msgstr "" #~ msgid "Sign in with %(provider)s" #~ msgstr "" #~ msgid "Username contains illegal characters" #~ msgstr "" #~ msgid "Sign out" #~ msgstr "" #~ msgid "Click here to reset your password" #~ msgstr "Clique aqui para redefinir a sua palavra-passe" #~ msgid "Click the link below to reset your password:" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s." #~ msgstr "" pallets-eco-flask-security-72bf929/flask_security/translations/pwl.txt000066400000000000000000000000501525721743500263540ustar00rootroot00000000000000token email thinsp qrcode authenticator pallets-eco-flask-security-72bf929/flask_security/translations/ru_RU/000077500000000000000000000000001525721743500260525ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/ru_RU/LC_MESSAGES/000077500000000000000000000000001525721743500276375ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/ru_RU/LC_MESSAGES/flask_security.po000066400000000000000000001642121525721743500332340ustar00rootroot00000000000000# Russian Translations for Flask-Security. # Copyright (C) 2017 CERN, leovp # This file is distributed under the same license as the Flask-Security # project. # FIRST AUTHOR , 2017. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 2.0.1\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: 2024-05-26 04:58+0530\n" "Last-Translator: Ivan Fedorov \n" "Language: ru_RU\n" "Language-Team: Leonid R. \n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "Подтвердите �вой новый email адре�" #: flask_security/core.py:363 msgid "Login Required" msgstr "Требует�� авторизаци�" #: flask_security/core.py:364 msgid "Welcome" msgstr "Добро пожаловать" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "Пожалуй�та, подтвердите �вой email" #: flask_security/core.py:366 msgid "Login instructions" msgstr "Ин�трукци� дл� входа" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "Ваш пароль был �брошен" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "Ваш пароль был изменён" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "Ин�трукции дл� во��тановлени� парол�" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:371 msgid "Your requested username" msgstr "" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:423 msgid "Verification Code" msgstr "Код подтверждени�" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "Ввод некорректен дл� запрошенного API" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" "�утентификаци� не удала�ь — идентификатор или пароль/код до�тупа " "недей�твительны" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" "Е�ли �тот адре� �лектронной почты е�ть в нашей �и�теме, вы получите " "пи�ьмо � опи�анием того, как �бро�ить пароль." #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "Е�ли �тот идентификатор е�ть в нашей �и�теме, вам был вы�лан код." #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "У ва� нет прав до�тупа к �тому ре�ур�у." #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "Вы должны авторизовать��, чтобы про�мотреть �тот ре�ур�." #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" "Спа�ибо. Чтобы подтвердить �вой email %(email)s, пожалуй�та, нажмите на " "��ылку в пи�ьме, которое мы только что отправили вам." #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "Спа�ибо. Ваш email был подтверждён." #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "Ваш email уже был подтверждён." #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "�еверный токен дл� подтверждени� аккаунта." #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s уже прив�зан к другому аккаунту." #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" "Идентификационный атрибут '%(attr)s' �о значением '%(value)s' уже " "а��оциирован � учетной запи�ью." #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "Идентификаци� %(id)s не зареги�трирована" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:531 msgid "Password does not match" msgstr "Пароль не подходит" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "Пароли не �овпадают" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "Перенаправлени� вне текущего домена запрещены" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "Код во��тановлени� недей�твителен" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "Коды во��тановлени� еще не �генерированы" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "Ин�трукции по во��тановлению парол� были отправлены на %(email)s." #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "Вы не �бро�или пароль в течение %(within)s. " #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "�еверный токен дл� во��тановлени� парол�." #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "Email требует подтверждени�." #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Ин�трукции по подтверждению были отправлены на %(email)s." #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "Вы не подтвердили �вое пи�ьмо в пределах %(within)s. " #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "Вы не вошли в течение %(within)s. �овые ин�трукции по входу отправлены на" " %(email)s." #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Ин�трукции по входу отправлены на %(email)s." #: flask_security/core.py:565 msgid "Invalid login token." msgstr "�еверный токен дл� входа." #: flask_security/core.py:566 msgid "Account is disabled." msgstr "�ккаунт отключён." #: flask_security/core.py:567 msgid "Email not provided" msgstr "Почтовый адре� не введён" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "�еверный Email" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "Код недей�твителен" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "" #: flask_security/core.py:578 msgid "Password not provided" msgstr "Пароль не введён" #: flask_security/core.py:580 #, python-format msgid "Password must be at least %(length)s characters" msgstr "Пароль должен �одержать не менее %(length)s �имволов" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "Пароль недо�таточно �ложный" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "Пароль в �пи�ке �компрометированных" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "�е удало�ь �оединить�� � �айтом �компрометированных паролей" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "�омер телефона некорректен, например, от�ут�твует код �траны" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "Указанный пользователь не �уще�твует" #: flask_security/core.py:591 msgid "Invalid password" msgstr "�еверный пароль" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "Предо�тавленный пароль или код недей�твительны" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "Вы у�пешно вошли в �и�тему." #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "Забыли пароль?" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "Вы у�пешно �бро�или пароль и автоматиче�ки вошли в �и�тему." #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" "Вы у�пешно �бро�или �вой пароль. Пожалуй�та, авторизуйте�ь, и�пользу� " "новый пароль." #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "Ваш новый пароль должен отличать�� от предыдущего." #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "Вы у�пешно изменили �вой пароль." #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "Пожалуй�та, войдите чтобы получить до�туп к �той �транице." #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "Пожалуй�та, войдите заново, чтобы получить до�туп к �той �транице." #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "Повторный вход выполнен у�пешно" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "Вы можете получить до�туп к данной �транице, только е�ли не авторизованы." #: flask_security/core.py:622 msgid "Code has been sent." msgstr "Код отправлен." #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "�е удало�ь отправить код. Пожалуй�та, попробуйте позже" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "Ваш код был подтвержден" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "Вы у�пешно изменили метод двухфакторной авторизации." #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "В на�то�щее врем� у ва� нет до�тупа к данной �транице" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "Отмеченный метод недей�твителен" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" "�а�тройка должна быть завершена в течение %(within)s. Пожалуй�та, начните" " заново." #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "�ктивные на данный момент варианты входа в �и�тему: %(method_list)s." #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "Запрошенный метод недей�твителен" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "�а�тройка единого �по�оба входа прошла у�пешно" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "Вы должны указать дей�твительный идентификатор дл� входа" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" "Им� пользовател� должно �одержать не менее %(min)d и не более %(max)d " "�имволов" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "Им� пользовател� может �одержать только буквы и цифры" #: flask_security/core.py:667 msgid "Username not provided" msgstr "Им� пользовател� не указано" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "Им� пользовател� %(username)s уже �в�зано � учётной запи�ью." #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "%(name)s не зареги�трирован � текущим пользователем." #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "�е�овпадение учётных данных пользовател�" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" "Подтверждение должно быть выполнено в течение %(within)s. Пожалуй�та, " "начните �начала." #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "Изменение email адре�а подтверждено" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" "Ин�трукции по подтверждению вашего нового email адре�а были отправлены на" " %(email)s." #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" "�а�троить � помощью приложени� дл� аутентификации (например google, " "lastpass, authy)" #: flask_security/forms.py:72 msgid "Change Method" msgstr "Изменить метод" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Сменить пароль" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "Код аутентификации" #: flask_security/forms.py:75 msgid "Delete" msgstr "Удалить" #: flask_security/forms.py:76 msgid "Email Address" msgstr "Email адре�" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "�а�троить � помощью �лектронной почты" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "Ошибка(и)" #: flask_security/forms.py:79 msgid "Identity" msgstr "Идентификатор" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Войти" #: flask_security/forms.py:81 msgid "New Password" msgstr "�овый пароль" #: flask_security/forms.py:82 msgid "Passcode" msgstr "Код до�тупа" #: flask_security/forms.py:83 msgid "Password" msgstr "Пароль" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "�омер телефона" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "Во��тановить пароль" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Зареги�трировать��" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "Запомнить мен�" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "Сбро�ить пароль" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "Подтверждение парол�" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "Отправить повторно ин�трукции по подтверждению аккаунта" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "Отправить ��ылку дл� входа" #: flask_security/forms.py:93 msgid "Send Code" msgstr "Отправить код" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "Войти" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "�а�троить � помощью СМС" #: flask_security/forms.py:96 msgid "Submit" msgstr "Отправить" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "Отправить код" #: flask_security/forms.py:98 msgid "Username" msgstr "Им� пользовател�" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "Подтвердите пароль" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "Google Authenticator" #: flask_security/forms.py:105 msgid "authenticator" msgstr "аутентификатор" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "email" #: flask_security/forms.py:108 msgid "SMS" msgstr "SMS" #: flask_security/forms.py:109 msgid "password" msgstr "пароль" #: flask_security/forms.py:110 msgid "passkey" msgstr "" #: flask_security/forms.py:111 msgid "none" msgstr "ничего" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "До�тупные методы" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "Проблемы � до�тупом к аккаунту?/Утер�но мобильное у�трой�тво?" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "Св�зать�� � админи�тратором" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "Показать коды во��тановлени�" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "Генераци� новых кодов во��тановлени�" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "Код во��тановлени�" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "До�тупные методы двухфакторной аутентификации:" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "Выбрать" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "Отправить код по email" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "И�пользовать ранее загруженный код во��тановлени�" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "Код или пароль" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "По �лектронной почте" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "По СМС" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "�а�троить дополнительный метод входа" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "Удаление активной опции входа" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "П�евдоним" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "И�пользование" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "И�пользовать как первичный метод аутентификации" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "И�пользовать как вторичный метод аутентификации" #: flask_security/webauthn.py:241 msgid "Start" msgstr "�ачать" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Меню" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "Изменить зареги�трированный Email" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "�а�тройка единого входа" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "Единый вход" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "Подтвердить аккаунт" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "По�ле отправки, на �тот новый email адре� будет отправлено подтверждение." #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "В на�то�щее врем� у ва� нет парол� — �то добавит его." #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Отправить ин�трукции по �бро�у парол�" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "или" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "Введите код во��тановлени�" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "Коды во��тановлени�" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" "Об�зательно �копируйте их и храните в надёжном ме�те. Каждый код может " "быть и�пользован только один раз." #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "Генераци� новых кодов во��тановлени�" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Сбро�ить пароль" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Заново отправить ин�трукции по подтверждению" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "Помимо имени пользовател� и парол�, вам нужно будет и�пользовать код." #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "Текущий на�троенный двухфакторный метод: %(method)s" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" "Откройте приложение аутентификатора на вашем у�трой�тве и про�канируйте " "�ледующий QR-код (или введите код ниже вручную), чтобы начать получать " "коды:" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "Введите код, чтобы завершить на�тройку" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "введите цифровой код" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "Это приложение поддерживает на�тройку кодов во��тановлени�." #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "Вы можете на�троить их зде�ь." #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "Пожалуй�та, введите код аутентификации, �генерированный через: %(method)s" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "Код аутентификации был отправлен вам на адре� �лектронной почты" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "�ам было отправлен email, чтобы �бро�ить на�тройки вашей учетной запи�и" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "�а�троить единый вход" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "Бе�парольный QR код" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "�икакие методы не были включены — нечего на�траивать" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "Введите код, чтобы завершить на�тройку" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "Запро�ить одноразовый код" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "Код отправлен" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" "Прозвище: «%s» И�пользование: «%s» Тран�порты: «%s» Обнаруживаемо�ть: " "«%s» Тип у�трой�тва: «%s» Резервное копирование «%s» По�леднее " "и�пользование: %s" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "Срок дей�тви� �той ��ылки и�текает через %(within)s." #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "Ваш текущий зареги�трированный email адре� — %(email)s." #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Ваш пароль был изменён." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Е�ли вы не мен�ли �вой пароль," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "нажмите �юда чтобы �бро�ить его" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" "Е�ли вы не мен�ли �вой пароль, то нажмите на ��ылку ниже дл� �бро�а " "парол�." #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "" #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Добро пожаловать, %(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Вы можете войти по ��ылке ниже:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Войти" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "не может получить до�туп к почтовой учетной запи�и" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "Здрав�твуйте %(email)s!" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" "Кто-то (вы?) попытал�� зареги�трировать �тот email, который уже е�ть в " "нашей �и�теме." #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" "С �той учетной запи�ью также �в�зано �ледующее им� пользовател�: " "%(username)s." #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" "Эта учётна� запи�ь также имеет �ледующее им� пользовател�, �в�занное �: " "%(username)s" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" "Вы попытали�ь зареги�трировать�� � именем пользовател� «%(username)s», " "которое уже �в�зано � другой учётной запи�ью." #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "Пожалуй�та, повторите проце�� реги�трации � другим именем пользовател�." #~ msgid "" #~ "Open your authenticator app on your " #~ "device and scan the following qrcode " #~ "to start receiving codes:" #~ msgstr "" #~ "Откройте ваше приложение дл� авторизации " #~ "на вашем у�трой�тве и про�канируйте " #~ "данный QR код чтобы начать получать " #~ "коды:" #~ msgid "Or use the the link below:" #~ msgstr "Или и�пользуйте данную ��ылку:" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ "Помимо вашего имени пользовател� и " #~ "парол�, вам нужно будет и�пользовать " #~ "код, который мы вам отправим" #~ msgid "Please enter your authentication code" #~ msgstr "Пожалуй�та, введите ваш код аутентификации" #~ msgid "Setup Unified Sign In options" #~ msgstr "�а�троить параметры единого входа" #~ msgid "Please re-authenticate" #~ msgstr "Пожалуй�та, войдите повторно чтобы получить до�туп к �той �транице." #~ msgid "Please Enter Your Password" #~ msgstr "Пожалуй�та, введите ваш пароль" #~ msgid "No password is set for this user" #~ msgstr "У данного пользовател� не у�тановлен пароль" #~ msgid "Invalid Token" #~ msgstr "Токен недей�твителен" #~ msgid "Your token has been confirmed" #~ msgstr "Ваш токен был подтвержден" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ "Откройте приложение дл� аутентификации на " #~ "�воем у�трой�тве и от�канируйте �ледующий " #~ "QR-код (или введите код ниже вручную)," #~ " чтобы начать получать пароли:" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ "П�евдоним: \"%s\" И�пользование: \"%s\" " #~ "Тран�порты: \"%s\" Возможно�ть обнаружени�: " #~ "\"%s\" По�леднее и�пользование: %s" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "Вы не во��тановили пароль в течение " #~ "%(within)s. �овые ин�трукции были отправлены" #~ " на %(email)s." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "Вы не подтвердили �вой почтовый адре�" #~ " в течение %(within)s. �овые ин�трукции " #~ "по подтверждению отправлены на %(email)s." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "Вы не аутентифицированы. Пожалуй�та, укажите корректные учётные данные." #~ msgid "Currently active sign in options:" #~ msgstr "�ктивные на данный момент варианты входа в �и�тему:" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ "Дл� завершени� входа введите код, " #~ "отправленный на вашу �лектронную почту" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "�а какой номер телефона необходимо отправить код?" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "�ам было отправлено пи�ьмо дл� �бро�а вашей учетной запи�и" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ "Произошла ошибка при взаимодей�твии � " #~ "провайдером Oauth. Пожалуй�та, попробуйте ещё" #~ " раз." #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "Спа�ибо. Ин�трукции по подтверждению были отправлены на %(email)s." #~ msgid "Two-factor Login" #~ msgstr "Двухфакторный вход" #~ msgid "Two-factor Rescue" #~ msgstr "Двухфакторное во��тановление" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Пожалуй�та, войдите повторно чтобы получить до�туп к �той �транице" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "Вы у�пешно отключили двухфакторную авторизацию." #~ msgid "Disable two factor authentication" #~ msgstr "Отключить двухфакторную аутентификацию" #~ msgid "Two Factor Setup" #~ msgstr "�а�тройка двухфакторной аутентификации" #~ msgid "Sign in with " #~ msgstr "Войти � помощью " #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "Выбрать метод двухфакторной аутентификации" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ "Двухфакторна� аутентификаци� добавл�ет " #~ "дополнительный уровень безопа�но�ти дл� вашей" #~ " учётной запи�и" #~ msgid "Two factor authentication code" #~ msgstr "Код двухфакторной аутентификации" #~ msgid "Two-factor Authentication" #~ msgstr "Двухфакторна� аутентификаци�" #~ msgid "Please Reauthenticate" #~ msgstr "Пожалуй�та, повторите аутентификацию" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ "Пожалуй�та, повторите аутентификацию, и�пользу� " #~ "ключ безопа�но�ти WebAuthn" #~ msgid "Change email" #~ msgstr "Изменить email" #~ msgid "Change password" #~ msgstr "Сменить пароль" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "Пожалуй�та, подтвердите �вой новый email адре�, нажав на ��ылку ниже:" #~ msgid "Confirm my new email" #~ msgstr "Подтвердить мой новый email" #~ msgid "Confirm my account" #~ msgstr "Подтвердить аккаунт" #~ msgid "You can log into your account using the following code:" #~ msgstr "Вы можете войти в �вою учётную запи�ь � помощью �ледующего кода:" #~ msgid "You can sign into your account using the following code:" #~ msgstr "Вы можете войти в �вою учетную запи�ь � помощью �ледующего кода:" #~ msgid "Or use the link below:" #~ msgstr "Или и�пользуйте ��ылку:" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "Пожалуй�та, подтвердите �вой новый email по ��ылке ниже:" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Пожалуй�та, подтвердите �вой email перейд� по ��ылке:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "Вы можете подтвердить �вой почтовый адре� перейд� по ��ылке:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "Е�ли вы забыли �вой пароль, вы можете во��тановить его" #~ msgid " here." #~ msgstr " тут." #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ "Е�ли вы забыли �вой пароль, вы " #~ "можете во��тановить его по �ледующей " #~ "��ылке:" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "И�пользуйте данный код дл� входа: %(code)s." #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ "Операци� WebAuthn должна быть завершена " #~ "в течение %(within)s. Пожалуй�та, начните " #~ "�начала." #~ msgid "Nickname for new credential is required." #~ msgstr "Требует�� п�евдоним дл� новых учётных данных." #~ msgid "%(name)s is already associated with a credential." #~ msgstr "%(name)s уже �в�зан � учётными данными." #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "Учётные данные WebAuthn � именем %(name)s у�пешно удалены" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "Учётные данные WebAuthn � именем %(name)s у�пешно добавлены" #~ msgid "WebAuthn credential id already registered." #~ msgstr "Учётные данные WebAuthn уже зареги�трированы." #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "�езареги�трированный идентификатор учетных данных WebAuthn." #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "Учётные данные WebAuthn не принадлежат ни одному пользователю." #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "�е удало�ь проверить учётные данные WebAuthn: %(cause)s." #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ "Учётные данные не зареги�трированы дл� " #~ "�того и�пользовани� (первичное или вторичное)" #~ msgid "webauthn" #~ msgstr "webauthn" #~ msgid "WebAuthn Setup" #~ msgstr "�а�тройка WebAuthn" #~ msgid "Forgot password" #~ msgstr "Забыли пароль" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "И�пользовать WebAuthn дл� входа" #~ msgid "Sign in with WebAuthn" #~ msgstr "Войти � помощью WebAuthn" #~ msgid "WebAuthn" #~ msgstr "WebAuthn" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "Это приложение поддерживает ключи безопа�но�ти WebAuthn." #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "И�пользовать ключ безопа�но�ти WebAuthn дл� повторной аутентификации" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "�а�троить новый ключ WebAuthn" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ "�ачните � предо�тавлени� уникального имени " #~ "дл� вашего нового ключа безопа�но�ти:" #~ msgid "Currently registered security keys:" #~ msgstr "Зареги�трированные ключи безопа�но�ти:" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "Удалить �уще�твующий ключ безопа�но�ти WebAuthn" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "Войти � помощью ключа безопа�но�ти WebAuthn" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ "И�пользовать ваш ключ безопа�но�ти WebAuthn" #~ " как вторичный метод духфакторной " #~ "аутентификации" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider: (%(exerror)s -" #~ " %(exdesc)s). Please try again." #~ msgstr "" #~ "При взаимодей�твии � провайдером Oauth " #~ "произошла ошибка: (%(exerror)s - %(exdesc)s)." #~ " Пожалуй�та, попробуйте ещё раз." #~ msgid "Use Social Oauth to Sign In" #~ msgstr "И�пользовать Social Oauth дл� входа" #~ msgid "Sign in with %(provider)s" #~ msgstr "" #~ msgid "Username contains illegal characters" #~ msgstr "Им� пользовател� �одержит недопу�тимые �имволы" #~ msgid "Sign out" #~ msgstr "Выход" #~ msgid "Click here to reset your password" #~ msgstr "�ажмите, чтобы �бро�ить �вой пароль" #~ msgid "Click the link below to reset your password:" #~ msgstr "�ажмите на ��ылку ниже дл� �бро�а парол�:" #~ msgid "You can sign in to your account using the following code: %(token)s" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s." #~ msgstr "" pallets-eco-flask-security-72bf929/flask_security/translations/tr_TR/000077500000000000000000000000001525721743500260505ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/tr_TR/LC_MESSAGES/000077500000000000000000000000001525721743500276355ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/tr_TR/LC_MESSAGES/flask_security.po000066400000000000000000001220641525721743500332310ustar00rootroot00000000000000# Turkish translation for Flask-Security. # Copyright (C) 2019 Ecmel B. Canlıer # This file is distributed under the same license as the Flask-Security # project. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 2.0.1\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: 2018-12-20 18:48+0300\n" "Last-Translator: Ecmel B. Canlıer \n" "Language: tr_TR\n" "Language-Team: \n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:363 msgid "Login Required" msgstr "Giriş yapmanız gerekmektedir" #: flask_security/core.py:364 msgid "Welcome" msgstr "Hoş Geldiniz" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "Lütfen e-posta adresinizi onaylayın" #: flask_security/core.py:366 msgid "Login instructions" msgstr "Giriş talimatları" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "Şifreniz yenilenmiştir" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "Şifreniz değiştirilmiştir" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "Şifre yenileme talimatları" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:371 msgid "Your requested username" msgstr "" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:423 msgid "Verification Code" msgstr "" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "Bu maddeyi görmeye yetkiniz yoktur." #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "Teşekkür ederiz. E-posta adresiniz onaylanmıştır" #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "E-posta adresiniz zaten onaylanmış." #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "Yanlış onaylama kodu." #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s başka bir hesaba bağlı." #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:531 msgid "Password does not match" msgstr "Şifre yanlış" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "Şifreler uymuyor" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "Adres dışına yönlendirmeler yasaktır" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "Şifrenizi yenileme talimatları %(email)s adresine gönderilmiştir." #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "Yanlış şifre yenileme kodu." #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "E-posta onayı gerekmektedir." #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "Onaylama talimatları %(email)s adresine gönderilmiştir." #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" "%(within)s içinde giriş yapmadınız. Yeni giriş yapma talimatları " "%(email)s adresine gönderilmiştir." #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "Giriş yapma talimatları %(email)s adresine gönderilmiştir." #: flask_security/core.py:565 msgid "Invalid login token." msgstr "Yanlış giriş kodu." #: flask_security/core.py:566 msgid "Account is disabled." msgstr "Hesap kapalıdır." #: flask_security/core.py:567 msgid "Email not provided" msgstr "E-posta verilmemiş" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "Yanlış e-posta adresi" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "" #: flask_security/core.py:578 msgid "Password not provided" msgstr "Şifre verilmemiş" #: flask_security/core.py:580 #, fuzzy, python-format msgid "Password must be at least %(length)s characters" msgstr "Şifreniz en az %(length)s karakter olmalıdır" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "Böyle bir kullanıcı yok" #: flask_security/core.py:591 msgid "Invalid password" msgstr "Şifre yanlış" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "Başarıyla giriş yaptınız." #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "Şifrenizi mi unuttunuz?" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "Şifreniz yenilenmiştir ve otomatik olarak giriş yapmış bulunmaktasınız." #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "Yeni şifreniz eski şifrenizden farklı olmalıdır." #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "Şifrenizi başarıyla değiştirdiniz." #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "Bu sayfaya erişebilmek için lütfen giriş yapın." #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "Bu sayfaya erişebilmek için lütfen tekrardan giriş yapın." #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "" #: flask_security/core.py:622 msgid "Code has been sent." msgstr "" #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "" #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:667 msgid "Username not provided" msgstr "" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" #: flask_security/forms.py:72 msgid "Change Method" msgstr "" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Şifre Değiştir" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "" #: flask_security/forms.py:75 msgid "Delete" msgstr "" #: flask_security/forms.py:76 msgid "Email Address" msgstr "E-posta Adresi" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "" #: flask_security/forms.py:79 msgid "Identity" msgstr "" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Giriş Yap" #: flask_security/forms.py:81 msgid "New Password" msgstr "Yeni Şifre" #: flask_security/forms.py:82 msgid "Passcode" msgstr "" #: flask_security/forms.py:83 msgid "Password" msgstr "Şifre" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "Şifre Kurtar" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Kayıt Ol" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "Beni Hatırla" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "Şifre Yenile" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "Şifre Tekrarı" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "Onaylama Talimatlarını Tekrar Gönder" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "Giriş Linki Gönder" #: flask_security/forms.py:93 msgid "Send Code" msgstr "" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "" #: flask_security/forms.py:96 msgid "Submit" msgstr "" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "" #: flask_security/forms.py:98 msgid "Username" msgstr "" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:105 msgid "authenticator" msgstr "" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "" #: flask_security/forms.py:108 msgid "SMS" msgstr "" #: flask_security/forms.py:109 msgid "password" msgstr "" #: flask_security/forms.py:110 msgid "passkey" msgstr "" #: flask_security/forms.py:111 msgid "none" msgstr "" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:241 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "Menü" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "Hesabı onayla" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "Şifre değiştirme talimatlarını gönder" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "Şifre yenile" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "Onaylama talimatlarını tekrar gönder" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Şifreniz değiştirilmiştir." #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "Eğer siz değiştirmediyseniz," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "buraya tıklayarak yenileyiniz" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "" #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Hoş Geldin %(email)s" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "Hesabına aşağıdaki linkten giriş yapabilirsin:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "Şimdi giriş yap" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" #~ msgid "You successfully confirmed password" #~ msgstr "" #~ msgid "Password confirmation is needed in order to access page" #~ msgstr "" #~ msgid "" #~ "Open your authenticator app on your " #~ "device and scan the following qrcode " #~ "to start receiving codes:" #~ msgstr "" #~ msgid "Or use the the link below:" #~ msgstr "" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "" #~ msgid "Please enter your authentication code" #~ msgstr "" #~ msgid "Setup Unified Sign In options" #~ msgstr "" #~ msgid "Please re-authenticate" #~ msgstr "Bu sayfaya erişebilmek için lütfen tekrardan giriş yapın." #~ msgid "Please Enter Your Password" #~ msgstr "" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "Bu kullanıcı için bir şifre yok" #~ msgid "Invalid Token" #~ msgstr "" #~ msgid "Your token has been confirmed" #~ msgstr "" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "" #~ "Şifrenizi %(within)s içinde yenilemediniz. " #~ "Yeni talimatlar %(email)s adresine " #~ "gönderilmiştir." #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "" #~ "E-posta adresinizi %(within)s içinde " #~ "onaylamadınız. Yeni onaylama talimatları " #~ "%(email)s adresine gönderilmiştir." #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "" #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "" #~ "Teşekkür ederiz. Onaylama talimatları " #~ "%(email)s adresine gönderilmiştir." #~ msgid "Two-factor Login" #~ msgstr "" #~ msgid "Two-factor Rescue" #~ msgstr "" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "Bu sayfaya erişebilmek için lütfen tekrardan giriş yapın." #~ msgid "You successfully disabled two factor authorization." #~ msgstr "" #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "" #~ msgid "Two factor authentication code" #~ msgstr "" #~ msgid "Two-factor Authentication" #~ msgstr "" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "Şifre değiştir" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "Hesabımı onayla" #~ msgid "You can log into your account using the following code:" #~ msgstr "" #~ msgid "You can sign into your account using the following code:" #~ msgstr "" #~ msgid "Or use the link below:" #~ msgstr "" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "Lütfen e-posta adresinizi aşağıdaki linkten onaylayınız:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "E-posta adresinizi aşağıdaki linkten onaylayabilirsiniz:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "Şifremi unuttum" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider: (%(exerror)s -" #~ " %(exdesc)s). Please try again." #~ msgstr "" #~ msgid "Use Social Oauth to Sign In" #~ msgstr "" #~ msgid "Sign in with %(provider)s" #~ msgstr "" #~ msgid "Username contains illegal characters" #~ msgstr "" #~ msgid "Sign out" #~ msgstr "" #~ msgid "Click here to reset your password" #~ msgstr "Şifreni yenilemek için buraya tıkla" #~ msgid "Click the link below to reset your password:" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s." #~ msgstr "" pallets-eco-flask-security-72bf929/flask_security/translations/zh_Hans_CN/000077500000000000000000000000001525721743500267705ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/zh_Hans_CN/LC_MESSAGES/000077500000000000000000000000001525721743500305555ustar00rootroot00000000000000flask_security.po000066400000000000000000001246431525721743500340770ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/flask_security/translations/zh_Hans_CN/LC_MESSAGES# Chinese (Simplified, China) translations for Flask-Security. # Copyright (C) 2017 CERN # This file is distributed under the same license as the Flask-Security # project. # FIRST AUTHOR , 2017. # msgid "" msgstr "" "Project-Id-Version: Flask-Security 2.0.1\n" "Report-Msgid-Bugs-To: info@inveniosoftware.org\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: 2018-08-02 19:55+0800\n" "Last-Translator: SteinKuo \n" "Language: zh_CN\n" "Language-Team: Chinese Simplified \n" "Plural-Forms: nplurals=1; plural=0;\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:363 msgid "Login Required" msgstr "需�登录" #: flask_security/core.py:364 msgid "Welcome" msgstr "欢迎" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "请激活你的电�邮箱" #: flask_security/core.py:366 msgid "Login instructions" msgstr "登录邮件" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "你的密�已�置" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "你的密�已更改" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "密��置" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "" #: flask_security/core.py:371 msgid "Your requested username" msgstr "" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:423 msgid "Verification Code" msgstr "验��" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "输入信��适�所请求的API" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "你无�查看此资��" #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "" #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "谢谢。你的邮箱已激活�" #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "你的邮箱已激活�" #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "无效验���" #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "%(email)s 已关�账户。" #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:531 msgid "Password does not match" msgstr "密��匹�" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "密��匹�" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "�止域�外�定�" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "�置密�邮件已��到 %(email)s。" #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "密��置验��无效�" #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "请先激活邮箱。" #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "激活邮件已��到 %(email)s。" #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "你未在 %(within)s 登录账户。新登录邮件已��到 %(email)s。" #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "登录邮件已��到 %(email)s。" #: flask_security/core.py:565 msgid "Invalid login token." msgstr "无效登录验���" #: flask_security/core.py:566 msgid "Account is disabled." msgstr "账户已被�用�" #: flask_security/core.py:567 msgid "Email not provided" msgstr "未填写电�邮箱" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "无效邮箱地�" #: flask_security/core.py:569 flask_security/core.py:624 #, fuzzy msgid "Invalid code" msgstr "密��正确" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "" #: flask_security/core.py:578 msgid "Password not provided" msgstr "未填写密�" #: flask_security/core.py:580 #, python-format msgid "Password must be at least %(length)s characters" msgstr "密�至少包�%(length)s个字符" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "密��够��" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "密�在易被泄露��上" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "未能连通检测易泄露密�的网站" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "手机���法。例如: 缺少国家代�" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "此用户�存在" #: flask_security/core.py:591 msgid "Invalid password" msgstr "密��正确" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "�交的密�或代�无效" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "你已�功登录�" #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "忘记密�?" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "你的密�已�功�置,并已自动登录。" #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "你的新密��能与当�密�相�。" #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "你已�功更改密��" #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "请登录访问此页�。" #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "请�新进行身份验�,以访问此页�。" #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "�功进行�新认�" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "您�能在未登录时访问此端点。" #: flask_security/core.py:622 msgid "Code has been sent." msgstr "" #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "��代�失败。请���试" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "你�功改�了你的�因素验�方法。" #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "你现在还没有��访问这个页�" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "选择的方法无效" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "必须在%(within)s内完�设置。请�新开始。" #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "" #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "�法的请求方法" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "统一登录设置�功" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "您必须指定一个有效的身份�能登录" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:667 msgid "Username not provided" msgstr "" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "设置一个认�的app(例如:google�lastpass�authy)" #: flask_security/forms.py:72 msgid "Change Method" msgstr "" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "更改密�" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "授��" #: flask_security/forms.py:75 msgid "Delete" msgstr "" #: flask_security/forms.py:76 msgid "Email Address" msgstr "邮箱地�" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "使用电�邮件进行设置" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "错误" #: flask_security/forms.py:79 msgid "Identity" msgstr "" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "登录" #: flask_security/forms.py:81 msgid "New Password" msgstr "新密�" #: flask_security/forms.py:82 msgid "Passcode" msgstr "" #: flask_security/forms.py:83 msgid "Password" msgstr "密�" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "手机�" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "��密�" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "注册" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "记�我" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "�置密�" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "�次确认密�" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "�新��邮件验�" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "��登录链接" #: flask_security/forms.py:93 msgid "Send Code" msgstr "��代�" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "登录" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "用SMS进行设置\"" #: flask_security/forms.py:96 msgid "Submit" msgstr "�交" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "�交代�" #: flask_security/forms.py:98 msgid "Username" msgstr "" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "验�密�" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:105 msgid "authenticator" msgstr "" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "" #: flask_security/forms.py:108 msgid "SMS" msgstr "" #: flask_security/forms.py:109 msgid "password" msgstr "" #: flask_security/forms.py:110 msgid "passkey" msgstr "" #: flask_security/forms.py:111 msgid "none" msgstr "" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "通过邮件" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "通过SMS" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:241 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "��" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "统一登陆" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "激活账户" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "��密��置邮件" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "�置密�" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "�新��激活邮件" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "打开设备上的身份验�应用,扫�以下二维�(或手动输入以下代�)开始接收代�:" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "认�代�已��至您的电�邮件地�。" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "�求��一次性代�" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "代�已���" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "你的密�已更改。" #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "如果你没更改你的密�," #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "点击这里�置密�" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "如果你没有更改密�,请点击下�的链接��置密�。" #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "" #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "欢迎你,%(email)s!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "你�以通过下�链接登录的你的账户:" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "立刻登录" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "无法进入邮箱" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" #~ msgid "You successfully confirmed password" #~ msgstr "您已�功确认密�" #~ msgid "Password confirmation is needed in order to access page" #~ msgstr "需�确认密��能访问页�" #~ msgid "" #~ "Open your authenticator app on your " #~ "device and scan the following qrcode " #~ "to start receiving codes:" #~ msgstr "打开设备上的验�器应用,扫�以下二维�然�接收代�:" #~ msgid "Or use the the link below:" #~ msgstr "" #~ msgid "Username not allowed" #~ msgstr "" #~ msgid "" #~ "In addition to your username and " #~ "password, you'll need to use a " #~ "code that we will send you" #~ msgstr "除了你的用户�和密�外,你还需�使用一个代�,这个代�我们将��给你" #~ msgid "Please enter your authentication code" #~ msgstr "请输入你的授��" #~ msgid "Setup Unified Sign In options" #~ msgstr "设置统一登录选项" #~ msgid "Please re-authenticate" #~ msgstr "请�新进行身份验�,以访问此页�。" #~ msgid "Please Enter Your Password" #~ msgstr "请输入你的密�" #~ msgid "Register WebAuthn Credential" #~ msgstr "" #~ msgid "No password is set for this user" #~ msgstr "此账户未设置密�" #~ msgid "Invalid Token" #~ msgstr "无效的令牌" #~ msgid "Your token has been confirmed" #~ msgstr "令牌已被确认" #~ msgid "" #~ "Open an authenticator app on your " #~ "device and scan the following QRcode " #~ "(or enter the code below manually) " #~ "to start receiving passcodes:" #~ msgstr "打开设备上的验�器应用,扫�以下二维�(或手动输入以下代�)开始接收代�:" #~ msgid "" #~ "Nickname: \"%s\" Usage: \"%s\" Transports: " #~ "\"%s\" Discoverable: \"%s\" Last used " #~ "on: %s" #~ msgstr "" #~ msgid "" #~ "You did not reset your password " #~ "within %(within)s. New instructions have " #~ "been sent to %(email)s." #~ msgstr "你未在 %(within)s �置密�。新�置密�邮件已��到 %(email)s。" #~ msgid "" #~ "You did not confirm your email " #~ "within %(within)s. New instructions to " #~ "confirm your email have been sent " #~ "to %(email)s." #~ msgstr "你未在 %(within)s 激活邮箱。新激活邮件已��到 %(email)s。" #~ msgid "You are not authenticated. Please supply the correct credentials." #~ msgstr "你还没有通过认�。请�供正确的凭�。" #~ msgid "Authenticator app" #~ msgstr "" #~ msgid "Email" #~ msgstr "" #~ msgid "None" #~ msgstr "" #~ msgid "Currently active sign in options:" #~ msgstr "" #~ msgid "To complete logging in, please enter the code sent to your mail" #~ msgstr "�完�登录,请输入��到您邮箱的代�" #~ msgid "To Which Phone Number Should We Send Code To?" #~ msgstr "我们应该将代���到哪个电���?" #~ msgid "enter code" #~ msgstr "" #~ msgid "A mail was sent to us in order to reset your application account" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider. Please try " #~ "again." #~ msgstr "" #~ msgid "Thank you. Confirmation instructions have been sent to %(email)s." #~ msgstr "谢谢你。已��激活邮件到 %(email)s。" #~ msgid "Two-factor Login" #~ msgstr "�因素认�登录" #~ msgid "Two-factor Rescue" #~ msgstr "" #~ msgid "You must re-authenticate to access this endpoint" #~ msgstr "请�新进行身份验�,以访问此页�。" #~ msgid "You successfully disabled two factor authorization." #~ msgstr "你�功地�用了�因素授�。" #~ msgid "Disable two factor authentication" #~ msgstr "" #~ msgid "Two Factor Setup" #~ msgstr "" #~ msgid "Sign in with " #~ msgstr "" #~ msgid "Username recovery" #~ msgstr "" #~ msgid "Select Two Factor Method" #~ msgstr "" #~ msgid "" #~ "Two-factor authentication adds an extra" #~ " layer of security to your account" #~ msgstr "�因素认�为您的账户增加了一层�外的安全�障。" #~ msgid "Two factor authentication code" #~ msgstr "�因素验�代�" #~ msgid "Two-factor Authentication" #~ msgstr "�因素授�" #~ msgid "Please Reauthenticate" #~ msgstr "" #~ msgid "Please Re-Authenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "Change email" #~ msgstr "" #~ msgid "Change password" #~ msgstr "更改密�" #~ msgid "Please confirm your new email address by clicking on the link below:" #~ msgstr "" #~ msgid "Confirm my new email" #~ msgstr "" #~ msgid "Confirm my account" #~ msgstr "激活账户" #~ msgid "You can log into your account using the following code:" #~ msgstr "您�以使用以下代�登录您的账户:" #~ msgid "You can sign into your account using the following code:" #~ msgstr "您�以使用以下代�登录您的账户。" #~ msgid "Or use the link below:" #~ msgstr "或者使用下�的链接:" #~ msgid "Please confirm your new email through the link below:" #~ msgstr "" #~ msgid "Please confirm your email through the link below:" #~ msgstr "请通过下�链接激活的你的邮箱:" #~ msgid "You can confirm your email through the link below:" #~ msgstr "你�以通过下�链接激活你的邮箱:" #~ msgid "If you forgot your password you can reset it" #~ msgstr "" #~ msgid " here." #~ msgstr "" #~ msgid "If you forgot your password you can reset it with the following link:" #~ msgstr "" #~ msgid "Use this code to sign in: %(code)s." #~ msgstr "使用此代�登录:%(code)s" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it " #~ " here." #~ msgstr "" #~ msgid "" #~ "If you forgot your password you " #~ "can reset it with the following " #~ "link: %(recovery_link)s" #~ msgstr "" #~ msgid "" #~ "WebAuthn operation must be completed " #~ "within %(within)s. Please start over." #~ msgstr "" #~ msgid "Nickname for new credential is required." #~ msgstr "" #~ msgid "%(name)s is already associated with a credential." #~ msgstr "" #~ msgid "Successfully deleted WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "Successfully added WebAuthn credential with name: %(name)s" #~ msgstr "" #~ msgid "WebAuthn credential id already registered." #~ msgstr "" #~ msgid "Unregistered WebAuthn credential id." #~ msgstr "" #~ msgid "WebAuthn credential doesn't belong to any user." #~ msgstr "" #~ msgid "Could not verify WebAuthn credential: %(cause)s." #~ msgstr "" #~ msgid "Credential not registered for this use (first or secondary)" #~ msgstr "" #~ msgid "webauthn" #~ msgstr "" #~ msgid "WebAuthn Setup" #~ msgstr "" #~ msgid "Forgot password" #~ msgstr "忘记密�" #~ msgid "Use WebAuthn to Sign In" #~ msgstr "" #~ msgid "Sign in with WebAuthn" #~ msgstr "" #~ msgid "WebAuthn" #~ msgstr "" #~ msgid "This application supports WebAuthn security keys." #~ msgstr "" #~ msgid "Use a WebAuthn Security Key to Reauthenticate" #~ msgstr "" #~ msgid "Setup New WebAuthn Security Key" #~ msgstr "" #~ msgid "Start by providing a unique name for your new security key:" #~ msgstr "" #~ msgid "Currently registered security keys:" #~ msgstr "" #~ msgid "Delete Existing WebAuthn Security Key" #~ msgstr "" #~ msgid "WebAuthn Security Key" #~ msgstr "" #~ msgid "Sign In Using WebAuthn Security Key" #~ msgstr "" #~ msgid "Use Your WebAuthn Security Key as a Second Factor" #~ msgstr "" #~ msgid "Reauthenticate Using Your WebAuthn Security Key" #~ msgstr "" #~ msgid "" #~ "An error occurred while communicating " #~ "with the Oauth provider: (%(exerror)s -" #~ " %(exdesc)s). Please try again." #~ msgstr "" #~ msgid "Use Social Oauth to Sign In" #~ msgstr "" #~ msgid "Sign in with %(provider)s" #~ msgstr "" #~ msgid "Username contains illegal characters" #~ msgstr "" #~ msgid "Sign out" #~ msgstr "" #~ msgid "Click here to reset your password" #~ msgstr "点击这里�置密�" #~ msgid "Click the link below to reset your password:" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s" #~ msgstr "" #~ msgid "You can sign in to your account using the following code: %(token)s." #~ msgstr "" pallets-eco-flask-security-72bf929/flask_security/twofactor.py000066400000000000000000000214161525721743500246630ustar00rootroot00000000000000""" flask_security.two_factor ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Flask-Security two_factor module :copyright: (c) 2016 by Gal Stainfeld, at Emedgene :copyright: (c) 2019-2025 by J. Christopher Wagner (jwag). """ from __future__ import annotations import typing as t from datetime import timedelta from flask import current_app, redirect, request, session from .forms import ( _setup_methods_xlate, get_form_field_xlate, DummyForm, TwoFactorRescueForm, ) from .proxies import _security, _datastore from .tf_plugin import TfPluginBase, tf_clean_session from .utils import ( _, SmsSenderFactory, base_render_json, _config_value as cv, do_flash, get_message, localize_callback, json_error_response, send_mail, url_for_security, _td_format, ) from .signals import ( tf_code_confirmed, tf_disabled, tf_security_token_sent, tf_profile_changed, ) if t.TYPE_CHECKING: # pragma: no cover import flask from flask_security import Security, UserMixin from flask.typing import ResponseValue def tf_send_security_token( user: UserMixin, method: str, totp_secret: str, phone_number: str | None ) -> None: """Sends the security token via email/sms for the specified user. :param user: The user to send the code to :param method: The method in which the code will be sent ('email' or 'sms', or 'authenticator') at the moment :param totp_secret: a unique shared secret of the user :param phone_number: If 'sms' phone number to send to There is no return value - it is assumed that exceptions are thrown by underlying methods that callers can catch. Flask-Security code should NOT call this directly - call :meth:`.UserMixin.tf_send_security_token` """ token_to_be_sent: str | None = _security.totp_factory.generate_totp_password( totp_secret ) if method == "email" or method == "mail": send_mail( cv("EMAIL_SUBJECT_TWO_FACTOR"), user.email, cv("TWO_FACTOR_EMAIL_TEMPLATE"), user=user, token=token_to_be_sent, username=user.calc_username(), within=_td_format(timedelta(seconds=cv("TWO_FACTOR_MAIL_VALIDITY"))), ) elif method == "sms": m, c = get_message("USE_CODE", code=token_to_be_sent) from_number = cv("SMS_SERVICE_CONFIG")["PHONE_NUMBER"] assert phone_number to_number = phone_number sms_sender = SmsSenderFactory.createSender(cv("SMS_SERVICE")) sms_sender.send_sms(from_number=from_number, to_number=to_number, msg=m) else: # password are generated automatically in the authenticator apps or not needed token_to_be_sent = None tf_security_token_sent.send( current_app._get_current_object(), # type: ignore[attr-defined] _async_wrapper=current_app.ensure_sync, user=user, method=method, token=token_to_be_sent, login_token=token_to_be_sent, phone_number=phone_number, ) def _complete_two_factor_process(user, primary_method, totp_secret, is_changing): """clean session according to process (login or changing two-factor method) and perform action accordingly """ _datastore.tf_set(user, primary_method, totp_secret=totp_secret) # if we are changing two-factor method dologin = False if is_changing: # As of 5.5.0 this is the legacy path (using session data) completion_message = "TWO_FACTOR_CHANGE_METHOD_SUCCESSFUL" tf_profile_changed.send( current_app._get_current_object(), _async_wrapper=current_app.ensure_sync, user=user, method=primary_method, ) # if we are logging in for the first time else: completion_message = "TWO_FACTOR_LOGIN_SUCCESSFUL" tf_code_confirmed.send( current_app._get_current_object(), _async_wrapper=current_app.ensure_sync, user=user, method=primary_method, ) dologin = True token = _security.two_factor_plugins.tf_complete(user, dologin) return completion_message, token def _set_rescue_options(form: TwoFactorRescueForm, user: UserMixin) -> dict[str, str]: # Based on config - set up options for rescue. # Note that this modifies the passed in Form as well as returns # a dict that can be returned as part of a JSON response. recovery_options = dict(help=url_for_security("two_factor_rescue")) if cv("TWO_FACTOR_RESCUE_EMAIL"): recovery_options["email"] = url_for_security("two_factor_rescue") assert isinstance(form.help_setup.choices, list) form.help_setup.choices.append( ("email", get_form_field_xlate(_("Send code via email"))) ) if ( _security.support_mfa and cv("MULTI_FACTOR_RECOVERY_CODES") and _datastore.mf_get_recovery_codes(user) ): recovery_options["recovery_code"] = url_for_security("mf_recovery") assert isinstance(form.help_setup.choices, list) form.help_setup.choices.append( ( "recovery_code", get_form_field_xlate(_("Use previously downloaded recovery code")), ) ) return recovery_options def tf_disable(user: UserMixin) -> None: """Disable two factor for user""" tf_clean_session() _datastore.tf_reset(user) tf_disabled.send( current_app._get_current_object(), # type: ignore[attr-defined] _async_wrapper=current_app.ensure_sync, user=user, ) def is_tf_setup(user: UserMixin) -> bool: """Return True is user account is setup for 2FA.""" return bool(user.tf_totp_secret and user.tf_primary_method) class CodeTfPlugin(TfPluginBase): def __init__(self, app: flask.Flask): super().__init__(app) def create_blueprint( self, app: flask.Flask, bp: flask.Blueprint, state: Security ) -> None: pass def get_setup_methods(self, user: UserMixin) -> list[tuple[str, str]]: if is_tf_setup(user): assert user.tf_primary_method is not None return [ ( user.tf_primary_method, localize_callback(_setup_methods_xlate[user.tf_primary_method]), ) ] return [] def tf_login( self, user: UserMixin, json_payload: dict[str, t.Any], next_loc: str | None ) -> ResponseValue: """Helper for two-factor authentication login This is called only when login/password have already been validated. This can be from login, register, confirm, unified sign in, unified magic link. If two-factor is already setup then this sends a code if the method requires it. If not, then user is redirected to two-factor-setup. In either case we do NOT log in user, so we must store some info in session to track our state (including what user). """ # if user's two-factor properties are not configured if not is_tf_setup(user): session["tf_state"] = "setup_from_login" json_payload["tf_state"] = "setup_from_login" if not _security._want_json(request): return redirect(url_for_security("two_factor_setup")) # if user's two-factor properties are configured else: session["tf_state"] = "ready" json_payload["tf_state"] = "ready" json_payload["tf_primary_method"] = user.tf_primary_method json_payload["tf_method"] = user.tf_primary_method if user.tf_primary_method in ["mail", "email", "sms"]: msg = user.tf_send_security_token( method=user.tf_primary_method, totp_secret=user.tf_totp_secret, phone_number=getattr(user, "tf_phone_number", None), ) if msg: # send code didn't work if not _security._want_json(request): # This is a mess - # we are deep down in the login/unified sign in flow. do_flash(msg, "error") return redirect(url_for_security("login")) else: payload = json_error_response(errors=msg) return _security._render_json(payload, 500, None, None) if not _security._want_json(request): values = dict(next=next_loc) if next_loc else dict() return redirect( url_for_security("two_factor_token_validation", **values) ) # JSON response - Fake up a form - doesn't really matter which. form = DummyForm(formdata=None) return base_render_json(form, include_user=False, additional=json_payload) pallets-eco-flask-security-72bf929/flask_security/unified_signin.py000066400000000000000000001162671525721743500256560ustar00rootroot00000000000000""" flask_security.unified_signin ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Flask-Security Unified Signin module :copyright: (c) 2019-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. This implements a unified sign in endpoint - allowing authentication via identity and passcode - where identity is configured via SECURITY_USER_IDENTITY_ATTRIBUTES, and allowable passcodes are one of US_ENABLED_METHODS. Finish up: - we should be able to add a phone number as part of setup even w/o any METHODS - i.e. to allow login with any identity (phone) and a password. Consider/Questions: - Allow registering/confirming with just a phone number - this likely would require a new register/confirm endpoint in order to implement verification. - Right now ChangePassword won't work - it requires an existing password - so if the user doesn't have one - can't change it. However ForgotPassword will in fact allow the user to add a password. Is that sufficient? - This also means that there is no way to REMOVE your password once it is setup, although user can register without one. - separate code validation times for SMS, email, authenticator? - token versus code versus passcode? Confusing terminology. """ from __future__ import annotations import time import typing as t from datetime import timedelta from flask import current_app from flask import after_this_request, request, session from flask_login import current_user from wtforms import ( BooleanField, PasswordField, RadioField, SelectMultipleField, StringField, SubmitField, TelField, validators, ) from wtforms.widgets import CheckboxInput from .confirmable import requires_confirmation from .decorators import anonymous_user_required, auth_required, unauth_csrf from .forms import ( _setup_methods_xlate, Form, NextFormMixin, RequiredLocalize, _build_form_from_request, _build_form, form_errors_munge, _generic_message, _get_form_field_label, get_form_field_xlate, IsString, IsStringOrInt, ) from .proxies import _security, _datastore from .quart_compat import get_quart_status from .signals import us_profile_changed, us_security_token_sent from .utils import ( _, SmsSenderFactory, base_render_json, check_and_get_token_status, _config_value as cv, _confirm_redirect, do_flash, get_identity_attributes, _get_post_login_redirect, _get_post_verify_redirect, get_message, get_url, _handle_already_auth, is_user_authenticated, localize_callback, login_user, lookup_identity, _propagate_next, send_mail, url_for_security, _view_commit, allowed_auth_token, _td_format, ) from .tf_plugin import tf_verify_validity_token from .twofactor import tf_clean_session from .webauthn import has_webauthn if t.TYPE_CHECKING: # pragma: no cover from flask.typing import ResponseReturnValue from flask_security import UserMixin if get_quart_status(): # pragma: no cover from quart import redirect else: from flask import redirect def _compute_code_methods(): # Return list of methods that actually send codes return list(set(cv("US_ENABLED_METHODS")) - {"password", "authenticator"}) def _compute_setup_methods(): # Return list of methods that require setup return list(set(cv("US_ENABLED_METHODS")) - {"password"}) def _compute_active_methods(user): # Compute methods already setup. active_methods = set(cv("US_ENABLED_METHODS")) & set( _datastore.us_get_totp_secrets(user).keys() ) if user.password: active_methods = active_methods.union({"password"}) return list(active_methods) def _compute_active_code_methods(user): return list(set(_compute_active_methods(user)) & set(_compute_code_methods())) def _us_common_validate(form): # Be aware - this has side effect on the form - it will fill in # the form.user # Validate identity - we go in order to figure out which user attribute the # request gave us. Note that we give up on the first 'match' even if that # doesn't yield a user. Why? form.user = lookup_identity(form.identity.data) if not form.user: form.identity.errors.append(get_message("US_SPECIFY_IDENTITY")[0]) return False if not form.user.is_active: form.identity.errors.append(get_message("DISABLED_ACCOUNT")[0]) return False if form.user.is_locked(form.identity.errors): return False return True class _UnifiedPassCodeForm(Form): """Common form for signin and verify/reauthenticate.""" # filled in by caller user: UserMixin # Filled in here authn_via: str # PasswordField so it doesn't show, no autocomplete since it might be a password # (could also be a passcode). passcode: PasswordField = PasswordField( _get_form_field_label("passcode"), render_kw={ "placeholder": get_form_field_xlate(_("Code or Password")), "autocomplete": "off", }, validators=[IsStringOrInt()], ) submit: SubmitField = SubmitField(_get_form_field_label("submit")) chosen_method: RadioField = RadioField( _("Available Methods"), choices=[ ("email", get_form_field_xlate(_("Via email"))), ("sms", get_form_field_xlate(_("Via SMS"))), ], validators=[IsString(), validators.Optional()], ) submit_send_code: SubmitField = SubmitField(_get_form_field_label("sendcode")) def __init__(self, *args: t.Any, **kwargs: t.Any): super().__init__(*args, **kwargs) def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False return True def validate2(self) -> bool: totp_secrets = _datastore.us_get_totp_secrets(self.user) if self.submit.data: # This is authn - verify passcode/password # Since we have a unique totp_secret for each method - we # can figure out which mechanism was used. # Note that password check requires a string (not int or None) assert isinstance(self.passcode.errors, list) passcode = self.passcode.data if not passcode: self.passcode.errors.append(get_message("INVALID_PASSWORD_CODE")[0]) return False passcode = str(passcode) ok = False for method in cv("US_ENABLED_METHODS"): if method == "password" and self.user.password: passcode = _security.password_util.normalize(passcode) if self.user.verify_and_update_password(passcode): ok = True break else: if method in totp_secrets and _security.totp_factory.verify_totp( token=passcode, totp_secret=totp_secrets[method], user=self.user, window=cv("US_TOKEN_VALIDITY"), ): ok = True break if not ok: self.user.track_failed_authn("passcode") self.passcode.errors.append(get_message("INVALID_PASSWORD_CODE")[0]) return False self.authn_via = method return True elif self.submit_send_code.data: # Send a code - chosen_method must be valid assert isinstance(self.chosen_method.errors, list) cm = self.chosen_method.data if cm not in cv("US_ENABLED_METHODS"): self.chosen_method.errors.append( get_message("US_METHOD_NOT_AVAILABLE")[0] ) return False if cm not in totp_secrets: self.chosen_method.errors.append( get_message("US_METHOD_NOT_AVAILABLE")[0] ) return False if cm == "sms" and not self.user.us_phone_number: # They need to us-setup! self.chosen_method.errors.append(get_message("PHONE_INVALID")[0]) return False return True return False # pragma: no cover class UnifiedSigninForm(_UnifiedPassCodeForm, NextFormMixin): """A unified login form For either identity/password or request and enter code. """ identity: StringField = StringField( _get_form_field_label("identity"), validators=[IsString(), RequiredLocalize()], ) remember: BooleanField = BooleanField( _get_form_field_label("remember_me"), default=lambda: cv("DEFAULT_REMEMBER_ME", app=current_app), ) def __init__(self, *args: t.Any, **kwargs: t.Any): super().__init__(*args, **kwargs) self.requires_confirmation: bool = False def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False if not _us_common_validate(self): return False if not super().validate2(): return False # Can't authenticate nor get a code if still required confirmation. self.requires_confirmation = requires_confirmation(self.user) assert isinstance(self.identity.errors, list) if self.requires_confirmation: self.identity.errors.append(get_message("CONFIRMATION_REQUIRED")[0]) return False return True class UnifiedVerifyForm(_UnifiedPassCodeForm, NextFormMixin): """Verify authentication. This is for freshness 'reauthentication' required. """ def __init__(self, *args: t.Any, **kwargs: t.Any): super().__init__(*args, **kwargs) if request and not self.next.data: self.next.data = request.args.get("next", "") def validate(self, **kwargs: t.Any) -> bool: self.user = current_user if not super().validate(**kwargs): return False if not super().validate2(): return False return True class UnifiedSigninSetupForm(Form): """Setup form""" setup_choices: list[tuple[str, str]] = [ ("email", _get_form_field_label("email_method")), ( "authenticator", _get_form_field_label("authapp_method"), ), ("sms", _get_form_field_label("sms_method")), ] chosen_method: RadioField = RadioField( get_form_field_xlate(_("Setup additional sign in option")), validate_choice=False, ) delete_choices: list[tuple[str, str]] = [ ("email", get_form_field_xlate("Delete email option")), ( "authenticator", get_form_field_xlate("Delete authenticator option"), ), ("sms", get_form_field_xlate("Delete SMS option")), ] delete_method: SelectMultipleField = SelectMultipleField( label=get_form_field_xlate(_("Delete active sign in option")), option_widget=CheckboxInput(), validate_choice=False, ) phone: TelField = TelField(_get_form_field_label("phone"), validators=[IsString()]) submit: SubmitField = SubmitField(_get_form_field_label("submit")) def __init__(self, *args: t.Any, **kwargs: t.Any): super().__init__(*args, **kwargs) def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False assert isinstance(self.chosen_method.errors, list) assert isinstance(self.phone.errors, list) assert isinstance(self.delete_method.errors, list) if not self.chosen_method.data and not self.delete_method.data: self.form_errors.append(get_message("API_ERROR")[0]) return False if self.chosen_method.data: if self.chosen_method.data not in cv("US_ENABLED_METHODS"): self.chosen_method.errors.append( get_message("US_METHOD_NOT_AVAILABLE")[0] ) return False if self.chosen_method.data == "sms": if not self.phone.data: self.phone.errors.append(get_message("PHONE_INVALID")[0]) return False msg = _security.phone_util.validate_phone_number(self.phone.data) if msg: self.phone.errors.append(msg) return False # As an identity attribute - it MUST be unique! cphone = _security.phone_util.get_canonical_form(self.phone.data) if _datastore.find_user(us_phone_number=cphone): msg = get_message( "IDENTITY_ALREADY_ASSOCIATED", attr="us_phone_number", value=cphone, )[0] self.phone.errors.append(msg) return False if self.delete_method.data: if not all( m in _compute_active_methods(current_user) for m in self.delete_method.data ): self.delete_method.errors.append( get_message("US_METHOD_NOT_AVAILABLE")[0] ) return False return True class UnifiedSigninSetupValidateForm(Form): """The unified sign in setup validation form""" # These 2 filled in by view user: UserMixin totp_secret: str passcode: StringField = StringField( _get_form_field_label("passcode"), render_kw={ "autocomplete": "one-time-code", "type": "text", "pattern": "[0-9]*", }, validators=[IsStringOrInt(), RequiredLocalize()], ) submit: SubmitField = SubmitField( _get_form_field_label("submitcode"), id="submit-code" ) def __init__(self, *args: t.Any, **kwargs: t.Any): super().__init__(*args, **kwargs) def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False assert isinstance(self.passcode.errors, list) assert self.passcode.data is not None # RequiredLocalize validator if not _security.totp_factory.verify_totp( token=self.passcode.data, totp_secret=self.totp_secret, user=self.user, window=cv("US_TOKEN_VALIDITY"), ): self.passcode.errors.append(get_message("INVALID_PASSWORD_CODE")[0]) return False return True def _send_code_helper(form, send_magic_link): # send code user = form.user method = form.chosen_method.data totp_secrets = _datastore.us_get_totp_secrets(user) msg = user.us_send_security_token( method, totp_secret=totp_secrets[method], phone_number=getattr(user, "us_phone_number", None), send_magic_link=send_magic_link, ) return msg @anonymous_user_required @unauth_csrf() def us_signin_send_code() -> ResponseReturnValue: """ Send code view. POST only. This takes an identity (as configured in USER_IDENTITY_ATTRIBUTES) and a method request to send a code. """ form = t.cast(UnifiedSigninForm, _build_form_from_request("us_signin_form")) form.submit_send_code.data = True form.submit.data = False code_methods = _compute_code_methods() if form.validate_on_submit(): msg = _send_code_helper(form, True) if msg: assert isinstance(form.chosen_method.errors, list) form.chosen_method.errors.append(msg) if _security._want_json(request): # Not authenticated yet - so don't send any user info. return base_render_json( form, include_user=False, error_status_code=500 if msg else 200 ) if not msg: # Make sure same response as non-setup method below do_flash(*_generic_message("CODE_HAS_BEEN_SENT", "GENERIC_US_SIGNIN")) return _security.render_template( cv("US_SIGNIN_TEMPLATE"), us_signin_form=form, available_methods=cv("US_ENABLED_METHODS"), code_methods=code_methods, chosen_method=form.chosen_method.data, skip_loginmenu=True, **_security._run_ctx_processor("us_signin"), ) elif request.method == "POST" and cv("RETURN_GENERIC_RESPONSES"): # TODO - this suppresses the error if they don't select ANY send code option. rinfo: dict[str, dict[str, str]] = dict( identity=dict(), passcode=dict(), chosen_method=dict() ) form_errors_munge(form, rinfo) if not _security._want_json(request): # Make sure same response as successful code above. do_flash(*get_message("GENERIC_US_SIGNIN")) # Here on failed validation if _security._want_json(request): payload = { "available_methods": cv("US_ENABLED_METHODS"), "code_methods": code_methods, "identity_attributes": get_identity_attributes(), } return base_render_json(form, include_user=False, additional=payload) if rurl := _confirm_redirect(form, "email"): return rurl return _security.render_template( cv("US_SIGNIN_TEMPLATE"), us_signin_form=form, available_methods=cv("US_ENABLED_METHODS"), code_methods=code_methods, skip_loginmenu=True, **_security._run_ctx_processor("us_signin"), ) @auth_required(lambda: cv("API_ENABLED_METHODS")) def us_verify_send_code() -> ResponseReturnValue: """ Send code during verify. POST only. """ form = t.cast(UnifiedVerifyForm, _build_form_from_request("us_verify_form")) form.submit_send_code.data = True form.submit.data = False code_methods = _compute_active_code_methods(current_user) if form.validate_on_submit(): msg = _send_code_helper(form, False) if msg: assert isinstance(form.chosen_method.errors, list) form.chosen_method.errors.append(msg) if _security._want_json(request): # no real reason to send user info? return base_render_json( form, include_user=False, error_status_code=500 if msg else 200 ) if not msg: do_flash(*get_message("CODE_HAS_BEEN_SENT")) return _security.render_template( cv("US_VERIFY_TEMPLATE"), us_verify_form=form, available_methods=cv("US_ENABLED_METHODS"), code_methods=code_methods, chosen_method=form.chosen_method.data, skip_login_menu=True, **_security._run_ctx_processor("us_verify"), ) # Here on failed validation if _security._want_json(request): return base_render_json(form) return _security.render_template( cv("US_VERIFY_TEMPLATE"), us_verify_form=form, available_methods=cv("US_ENABLED_METHODS"), code_methods=code_methods, skip_login_menu=True, **_security._run_ctx_processor("us_verify"), ) @unauth_csrf() def us_signin() -> ResponseReturnValue: """ Unified sign in view. This takes an identity (as configured in USER_IDENTITY_ATTRIBUTES) and a passcode (password or OTP). """ form = t.cast(UnifiedSigninForm, _build_form_from_request("us_signin_form")) form.submit.data = True form.submit_send_code.data = False code_methods = _compute_code_methods() payload = { "available_methods": cv("US_ENABLED_METHODS"), "code_methods": code_methods, "identity_attributes": get_identity_attributes(), } if is_user_authenticated(current_user): return _handle_already_auth(form, payload=payload) # Clean out any potential old session info - in case of previous # aborted 2FA attempt. tf_clean_session() if form.validate_on_submit(): # Check if multifactor is required. Some (this is configurable) don't # need 2FA since they ARE multifactor (such as SMS and authenticator). remember_me = form.remember.data if "remember" in form else None if form.authn_via in cv("US_MFA_REQUIRED"): response = _security.two_factor_plugins.tf_enter( form.user, remember_me, form.authn_via, next_loc=_propagate_next(request.url, form), ) if response: return response after_this_request(_view_commit) login_user(form.user, remember=remember_me, authn_via=[form.authn_via]) if _security._want_json(request): return base_render_json( form, include_auth_token=allowed_auth_token(form.user), additional=dict(tf_required=False), ) return redirect(_get_post_login_redirect()) # Here on GET or failed POST validate if request.method == "POST" and cv("RETURN_GENERIC_RESPONSES"): rinfo = dict( identity=dict(replace_msg="GENERIC_AUTHN_FAILED"), passcode=dict(replace_msg="GENERIC_AUTHN_FAILED"), ) form_errors_munge(form, rinfo) if request.method == "GET": # set CSRF COOKIE if configured. This is the equivalent of forms and # base_render_json always sending the csrf_token session["fs_cc"] = "set" if _security._want_json(request): return base_render_json(form, include_user=False, additional=payload) # On error - wipe code form.passcode.data = None if rurl := _confirm_redirect(form, "email"): return rurl return _security.render_template( cv("US_SIGNIN_TEMPLATE"), us_signin_form=form, available_methods=cv("US_ENABLED_METHODS"), code_methods=code_methods, skip_login_menu=True, **_security._run_ctx_processor("us_signin"), ) @auth_required(lambda: cv("API_ENABLED_METHODS")) def us_verify() -> ResponseReturnValue: """ Re-authenticate to reset freshness time. This is likely the result of a reauthn_handler redirect, which will have filled in ?next=xxx - which we want to carefully not lose as we go through these steps. """ form = t.cast(UnifiedVerifyForm, _build_form_from_request("us_verify_form")) form.submit.data = True form.submit_send_code.data = False code_methods = _compute_active_code_methods(current_user) if form.validate_on_submit(): # verified - so set freshness time. session["fs_paa"] = time.time() if _security._want_json(request): return base_render_json( form, include_auth_token=allowed_auth_token(form.user) ) do_flash(*get_message("REAUTHENTICATION_SUCCESSFUL")) return redirect(_get_post_verify_redirect()) # Here on GET or failed POST validate webauthn_available = has_webauthn(current_user, cv("WAN_ALLOW_AS_VERIFY")) if _security._want_json(request): payload = { "available_methods": cv("US_ENABLED_METHODS"), "code_methods": code_methods, "has_webauthn_verify_credential": webauthn_available, "oauth_enabled": cv("OAUTH_ENABLE"), "oauth_providers": ( _security.oauthglue.provider_names # type: ignore[union-attr] if cv("OAUTH_ENABLE") else [] ), } return base_render_json(form, additional=payload) # On error - wipe code form.passcode.data = None return _security.render_template( cv("US_VERIFY_TEMPLATE"), us_verify_form=form, code_methods=code_methods, skip_login_menu=True, has_webauthn_verify_credential=webauthn_available, wan_verify_form=_build_form("wan_verify_form"), **_security._run_ctx_processor("us_verify"), ) @anonymous_user_required def us_verify_link() -> ResponseReturnValue: """ Used to verify a magic email link. GET only Since this is just a URL - be careful not to disclose info like whether email exists or not. """ fs_uniquifier = request.args.get("id", None) code = request.args.get("code", None) if not fs_uniquifier or not code: m, c = get_message("API_ERROR") if cv("REDIRECT_BEHAVIOR") == "spa": return redirect(get_url(cv("LOGIN_ERROR_VIEW"), qparams={c: m})) do_flash(m, c) return redirect(url_for_security("us_signin")) user = _datastore.find_user(fs_uniquifier=fs_uniquifier) if not user or not user.active: if not user: m, c = _generic_message("USER_DOES_NOT_EXIST", "GENERIC_AUTHN_FAILED") else: m, c = _generic_message("DISABLED_ACCOUNT", "GENERIC_AUTHN_FAILED") if cv("REDIRECT_BEHAVIOR") == "spa": return redirect(get_url(cv("LOGIN_ERROR_VIEW"), qparams={c: m})) do_flash(m, c) return redirect(url_for_security("us_signin")) totp_secrets = _datastore.us_get_totp_secrets(user) if "email" not in totp_secrets or not _security.totp_factory.verify_totp( token=code, totp_secret=totp_secrets["email"], user=user, window=cv("US_TOKEN_VALIDITY"), ): m, c = _generic_message("INVALID_CODE", "GENERIC_AUTHN_FAILED") user.track_failed_authn("passcode") if cv("REDIRECT_BEHAVIOR") == "spa": return redirect( get_url( cv("LOGIN_ERROR_VIEW"), qparams=user.get_redirect_qparams({c: m}), ) ) do_flash(m, c) return redirect(url_for_security("us_signin")) # copied from tf_enter... if _security.support_mfa: tf_setup_methods = _security.two_factor_plugins.get_setup_tf_methods(user) tf_fresh = tf_verify_validity_token(user.fs_uniquifier) tf_required, tf_available_methods = user.check_tf_required( tf_setup_methods, tf_fresh ) if tf_required and "email" in cv("US_MFA_REQUIRED"): # tf_login doesn't know anything about "spa" etc. In general two-factor # isn't quite ready for SPA. So we return an error via a redirect rather # than mess up SPA applications. To be clear - this simply doesn't # work - using a magic link w/ 2FA - need to use code/password as primary # authentication. if cv("REDIRECT_BEHAVIOR") == "spa": return redirect( get_url( cv("LOGIN_ERROR_VIEW"), qparams=user.get_redirect_qparams({"tf_required": 1}), ) ) response = _security.two_factor_plugins.tf_enter( user, False, "email", next_loc=_propagate_next(request.url, None) ) if response: return response login_user(user, authn_via=["email"]) after_this_request(_view_commit) if cv("REDIRECT_BEHAVIOR") == "spa": # We do NOT send the authentication token here since the only way to # send it would be via a query param and that isn't secure. (logging and # possibly HTTP Referer header). # This means that this can only work if sessions are active which sort of # makes sense - otherwise you need to use /us-signin with a code. return redirect( get_url(cv("POST_LOGIN_VIEW"), qparams=user.get_redirect_qparams()) ) do_flash(*get_message("PASSWORDLESS_LOGIN_SUCCESSFUL")) return redirect(_get_post_login_redirect()) @auth_required( lambda: cv("API_ENABLED_METHODS"), within=lambda: cv("FRESHNESS"), grace=lambda: cv("FRESHNESS_GRACE_PERIOD"), ) def us_setup() -> ResponseReturnValue: """ Change unified sign in methods. We want to verify the new method - so don't store anything yet in DB use a timed signed token to pass along state. GET - retrieve current info (json) or form. """ form = t.cast(UnifiedSigninSetupForm, _build_form_from_request("us_setup_form")) setup_methods = _compute_setup_methods() active_methods = _compute_active_methods(current_user) form.chosen_method.choices = [ c for c in form.setup_choices if c[0] not in active_methods and c[0] in setup_methods ] form.delete_method.choices = [ c for c in form.delete_choices if c[0] in active_methods ] # translate active methods if not active_methods: active_methods = [None] current_methods = _security.i18n_domain.format_list( [localize_callback(_setup_methods_xlate[m]) for m in active_methods] ) current_methods_msg = get_message( "US_CURRENT_METHODS", method_list=current_methods )[0] if form.validate_on_submit(): qrcode_values = dict() json_response = dict() state_token = None delete_method = form.delete_method.data add_method = form.chosen_method.data if delete_method: after_this_request(_view_commit) for m in delete_method: _datastore.us_reset(current_user, m) active_methods = _compute_active_methods(current_user) if not active_methods: active_methods = [None] current_methods = _security.i18n_domain.format_list( [localize_callback(_setup_methods_xlate[m]) for m in active_methods] ) current_methods_msg = get_message( "US_CURRENT_METHODS", method_list=current_methods )[0] form.chosen_method.choices = [ c for c in form.setup_choices if c[0] not in active_methods and c[0] in setup_methods ] form.delete_method.choices = [ c for c in form.delete_choices if c[0] in active_methods ] form.delete_method.data = None us_profile_changed.send( current_app._get_current_object(), # type: ignore _async_wrapper=current_app.ensure_sync, # type: ignore[arg-type] user=current_user, methods=delete_method, delete=True, ) if add_method: # Always generate a totp_secret. We don't set it in the DB until # user has successfully validated. totp = _security.totp_factory.generate_totp_secret() # N.B. totp (totp_secret) is actually encrypted - so it seems safe enough # to send it to the user. # Only check phone number if SMS (see form validate) phone_number = None if add_method == "sms": assert form.phone.data is not None phone_number = _security.phone_util.get_canonical_form(form.phone.data) state = { "totp_secret": totp, "chosen_method": add_method, "phone_number": phone_number, } msg = current_user.us_send_security_token( method=add_method, totp_secret=totp, phone_number=phone_number, ) if msg: # sending didn't work. assert isinstance(form.chosen_method.errors, list) form.chosen_method.errors.append(msg) if _security._want_json(request): # Not authenticated yet - so don't send any user info. return base_render_json( form, include_user=False, error_status_code=500 if msg else 400 ) return _security.render_template( cv("US_SETUP_TEMPLATE"), available_methods=cv("US_ENABLED_METHODS"), active_methods=active_methods, current_methods_msg=current_methods_msg, setup_methods=setup_methods, us_setup_form=form, **_security._run_ctx_processor("us_setup"), ) state_token = _security.us_setup_serializer.dumps(state) json_response = dict( chosen_method=form.chosen_method.data, phone=phone_number, state=state_token, ) if form.chosen_method.data == "authenticator": authr_setup_values = _security.totp_factory.fetch_setup_values( totp, current_user ) # Add all the values used in qrcode to json response json_response["authr_key"] = authr_setup_values["key"] json_response["authr_b32key"] = authr_setup_values["b32key"] json_response["authr_username"] = authr_setup_values["username"] json_response["authr_issuer"] = authr_setup_values["issuer"] json_response["authr_uri"] = authr_setup_values["uri"] qrcode_values = dict( authr_qrcode=authr_setup_values["image"], authr_key=authr_setup_values["key"], authr_username=authr_setup_values["username"], authr_issuer=authr_setup_values["issuer"], ) if _security._want_json(request): return base_render_json(form, include_user=False, additional=json_response) form.delete_method.data = None return _security.render_template( cv("US_SETUP_TEMPLATE"), available_methods=cv("US_ENABLED_METHODS"), active_methods=active_methods, current_methods_msg=current_methods_msg, setup_methods=setup_methods, code_sent=form.chosen_method.data in _compute_code_methods(), chosen_method=form.chosen_method.data, us_setup_form=form, us_setup_validate_form=_build_form("us_setup_validate_form"), **qrcode_values, state=state_token, **_security._run_ctx_processor("us_setup"), ) phone_number = None if "sms" in cv("US_ENABLED_METHODS"): phone_number = current_user.us_phone_number # Get here on initial new setup (GET) # Or failure of POST if _security._want_json(request): payload = { "identity_attributes": get_identity_attributes(), "available_methods": cv("US_ENABLED_METHODS"), "active_methods": active_methods, "setup_methods": setup_methods, "phone": phone_number, } return base_render_json(form, include_user=False, additional=payload) # Show user existing phone number form.phone.data = phone_number form.chosen_method.data = None form.delete_method.data = None return _security.render_template( cv("US_SETUP_TEMPLATE"), available_methods=cv("US_ENABLED_METHODS"), active_methods=active_methods, current_methods_msg=current_methods_msg, setup_methods=setup_methods, us_setup_form=form, **_security._run_ctx_processor("us_setup"), ) @auth_required(lambda: cv("API_ENABLED_METHODS")) def us_setup_validate(token: str) -> ResponseReturnValue: """ Validate new setup. The token is the state variable that is signed and timed and contains all the state that once confirmed will be stored in the user record. """ form = t.cast( UnifiedSigninSetupValidateForm, _build_form_from_request("us_setup_validate_form"), ) expired, invalid, state = check_and_get_token_status( token, "us_setup", cv("US_SETUP_WITHIN") ) if invalid: m, c = get_message("API_ERROR") if expired: m, c = get_message("US_SETUP_EXPIRED", within=_td_format(cv("US_SETUP_WITHIN"))) if invalid or expired: if _security._want_json(request): form.form_errors.append(m) return base_render_json(form, include_user=False) do_flash(m, c) return redirect(url_for_security("us_setup")) form.totp_secret = state["totp_secret"] form.user = current_user if form.validate_on_submit(): after_this_request(_view_commit) method = state["chosen_method"] phone = state["phone_number"] if method == "sms" else None _datastore.us_set(current_user, method, state["totp_secret"], phone) us_profile_changed.send( current_app._get_current_object(), # type: ignore _async_wrapper=current_app.ensure_sync, # type: ignore[arg-type] user=current_user, methods=[method], delete=False, ) if _security._want_json(request): phone_number = None if "sms" in cv("US_ENABLED_METHODS"): phone_number = current_user.us_phone_number return base_render_json( form, include_user=False, additional=dict(chosen_method=method, phone=phone_number), ) else: do_flash(*get_message("US_SETUP_SUCCESSFUL")) return redirect(get_url(cv("US_POST_SETUP_VIEW"))) # Code not correct/outdated. if _security._want_json(request): return base_render_json(form, include_user=False) m, c = get_message("INVALID_PASSWORD_CODE") do_flash(m, c) return redirect(url_for_security("us_setup")) def us_send_security_token( user: UserMixin, method: str, totp_secret: str, phone_number: str | None, send_magic_link: bool = False, ) -> None: """Generate and send the security code. :param user: The user to send the code to :param method: The method in which the code will be sent :param totp_secret: the unique shared secret of the user :param phone_number: If 'sms' phone number to send to :param send_magic_link: If true a magic link that can be clicked on will be sent. This shouldn't be sent during a setup. There is no return value - it is assumed that exceptions are thrown by underlying methods that callers can catch. Flask-Security code should NOT call this directly - call :meth:`.UserMixin.us_send_security_token` .. versionadded:: 3.4.0 """ code: str | None = _security.totp_factory.generate_totp_password(totp_secret) if method == "email": login_link = None if send_magic_link: login_link = url_for_security( "us_verify_link", id=str(user.fs_uniquifier), code=code, _external=True ) send_mail( cv("US_EMAIL_SUBJECT"), user.email, cv("US_EMAIL_TEMPLATE"), user=user, username=user.calc_username(), token=code, # deprecated login_token=code, login_link=login_link, within=_td_format(timedelta(seconds=cv("US_TOKEN_VALIDITY"))), ) elif method == "sms": m, c = get_message("USE_CODE", code=code) from_number = cv("SMS_SERVICE_CONFIG")["PHONE_NUMBER"] assert phone_number to_number = phone_number sms_sender = SmsSenderFactory.createSender(cv("SMS_SERVICE")) sms_sender.send_sms(from_number=from_number, to_number=to_number, msg=m) elif method == "authenticator" or method == "password": # tokens are generated automatically with authenticator apps # and passwords are well passwords # Still go ahead and notify signal receivers that they requested it. code = None us_security_token_sent.send( current_app._get_current_object(), # type: ignore[attr-defined] _async_wrapper=current_app.ensure_sync, user=user, method=method, token=code, login_token=code, phone_number=phone_number, send_magic_link=send_magic_link, ) pallets-eco-flask-security-72bf929/flask_security/username_util.py000066400000000000000000000051751525721743500255330ustar00rootroot00000000000000""" flask_security.username_util ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Utility class providing methods for validating and normalizing usernames. :copyright: (c) 2020-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from __future__ import annotations import typing as t from .utils import ( _config_value as cv, get_message, input_svn, ) if t.TYPE_CHECKING: # pragma: no cover import flask class UsernameUtil: """ Utility class providing methods for validating and normalizing usernames. To provide your own implementation, pass in the class as ``username_util_cls`` at init time. Your class will be instantiated once as part of app initialization. .. versionadded:: 4.1.0 """ def __init__(self, app: flask.Flask): """Instantiate class. :param app: The Flask application being initialized. """ pass def normalize(self, username: str) -> str | None: """ Given an input username - return a clean (using nh3) and normalized (using Python's unicodedata.normalize()) version. Must be called in app context and uses :py:data:`SECURITY_INPUT_NORMALIZE_FORM` and :py:data:`SECURITY_USERNAME_ALLOWED_CHARS` config variables. """ reason, clean_input = input_svn( username, cv("USERNAME_ALLOWED_CHARS"), cv("INPUT_NORMALIZE_FORM") ) return clean_input def validate(self, username: str) -> tuple[str | None, str | None]: """ Username validation. Called in app/request context. The username is first validated then normalized. Return value is a tuple (msg, normalized_username). msg will be None if properly validated. It is important that None be returned if data is an empty string since otherwise DBs will complain since the field is unique/nullable. """ if not username: return None, None reason, clean_input = input_svn( username, cv("USERNAME_ALLOWED_CHARS"), cv("INPUT_NORMALIZE_FORM") ) if clean_input is None: msg = ( "USERNAME_DISALLOWED_CHARACTERS" if reason == "unallowed" else "INVALID_INPUT" ) return get_message(msg)[0], None umin = cv("USERNAME_MIN_LENGTH") umax = cv("USERNAME_MAX_LENGTH") if len(clean_input) < umin or len(clean_input) > umax: return ( get_message("USERNAME_INVALID_LENGTH", min=umin, max=umax)[0], clean_input, ) return None, clean_input pallets-eco-flask-security-72bf929/flask_security/utils.py000066400000000000000000001503121525721743500240110ustar00rootroot00000000000000""" flask_security.utils ~~~~~~~~~~~~~~~~~~~~ Flask-Security utils module :copyright: (c) 2012-2019 by Matt Wright. :copyright: (c) 2019-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from __future__ import annotations import abc import base64 import unicodedata from datetime import datetime, timedelta, timezone from functools import partial import hashlib import hmac import time import typing as t from urllib.parse import parse_qsl, quote, urlsplit, urlunsplit, urlencode import urllib.request import urllib.error from flask import ( Response, current_app, flash, g, redirect, request, render_template, session, url_for, after_this_request, ) from flask_login import login_user as _login_user from flask_login import logout_user as _logout_user from flask_login import current_user from flask_login import COOKIE_NAME as REMEMBER_COOKIE_NAME from flask_principal import AnonymousIdentity, Identity, identity_changed, Need from flask_wtf import csrf, FlaskForm from wtforms import ValidationError from itsdangerous import BadSignature, SignatureExpired from werkzeug.local import LocalProxy from werkzeug.datastructures import MultiDict from .quart_compat import best, get_quart_status from .proxies import _security, _datastore, _pwd_context, _hashing_context from .signals import user_authenticated if t.TYPE_CHECKING: # pragma: no cover from flask import Flask from flask.typing import ResponseValue from flask_security import UserMixin localize_callback: t.Callable[..., str] = LocalProxy( lambda: _security.i18n_domain.gettext ) _FsPermNeed = partial(Need, "fsperm") _FsPermNeed.__doc__ = """A need with the method preset to `"fsperm"`.""" def _(translate: str) -> str: """Identity function to mark strings for translation.""" return translate def get_request_attr(name: str) -> t.Any: """Retrieve a request local attribute. Current public attributes are: **fs_authn_via** will be set to the authentication mechanism (session, token, basic) that the current request was authenticated with. Returns None if attribute doesn't exist. .. versionadded:: 4.0.0 .. versionchanged:: 4.1.5 Use 'g' rather than request_ctx stack which is going away post Flask 2.2 """ return getattr(g, name, None) def set_request_attr(name: str, value: t.Any) -> None: """Set an attribute on Flask's application context global object (g). :param name: The key/attribute name to store the value under :param value: The value to store in the application context """ return setattr(g, name, value) """ Most view functions that modify the DB will call ``after_this_request(view_commit)`` Quart compatibility needs an async version """ if get_quart_status(): # pragma: no cover async def _view_commit(response=None): _datastore.commit() return response else: def _view_commit(response=None): # type: ignore[misc] _datastore.commit() return response def aware_utcnow() -> datetime: """Return a timezone-aware UTC datetime object. From a miguel grinberg blog around dealing with 3.12. Our default SQLAlchemy Datetime is naive. Note that most code should call _security.datetime_factory() :return: Current UTC datetime with timezone information :rtype: datetime """ return datetime.now(timezone.utc) def aware_utcfromtimestamp(timestamp: float) -> datetime: """Create timezone-aware UTC datetime from timestamp. :param timestamp: Unix timestamp (seconds since epoch) :type timestamp: float :return: UTC datetime with timezone information :rtype: datetime """ return datetime.fromtimestamp(timestamp, timezone.utc) def naive_utcnow() -> datetime: """Return a naive UTC datetime (tzinfo removed). :return: Current UTC datetime without timezone information :rtype: datetime """ return aware_utcnow().replace(tzinfo=None) def naive_utcfromtimestamp(timestamp: float) -> datetime: """Create naive UTC datetime from timestamp. :param timestamp: Unix timestamp (seconds since epoch) :type timestamp: float :return: UTC datetime without timezone information :rtype: datetime """ return aware_utcfromtimestamp(timestamp).replace(tzinfo=None) def _find_csrf_field_name() -> t.Optional[str]: """Retrieve the configured CSRF field name from Flask-WTF form configuration. This is needed to properly clear CSRF tokens on logout since Flask-WTF doesn't automatically handle this case. The field name can be configured through Flask-WTF's settings or overridden in form classes. :return: Configured CSRF field name if found, None otherwise :rtype: Optional[str] Note: Uses the field name from the login form as set by the Flask-WTF configuration. Requires a DummyForm class with Flask-WTF's meta configuration. """ from .forms import DummyForm form = DummyForm(formdata=None) if hasattr(form.meta, "csrf_field_name"): return form.meta.csrf_field_name return None def is_user_authenticated(user: UserMixin | None) -> bool: """ return True if user is authenticated. With Flask-Login <=0.6.x and Flask-Security <5.4 current_user was always set - for non-authenticated users it pointed to an AnonymousUser Flask-Login is experimenting (11/5/23) with a LOGIN_NO_ANONYMOUS which will set current_user to None and deprecate is_authenticated (current_user non None implies authenticated). We have a configuration variable ANONYMOUS_USER_DISABLED which if true will force current_user to None on unauthenticated as well """ if _config_value("ANONYMOUS_USER_DISABLED"): # Note that user often is current_user which is a proxy and isn't ever actually # 'None' return bool(user) return bool(user and user.is_authenticated) def login_user( user: UserMixin, remember: bool | None = None, authn_via: list[str] | None = None, ) -> bool: """Perform the login routine. If :py:data:`SECURITY_TRACKABLE` is used, make sure you commit changes after this request (i.e. ``app.security.datastore.commit()``). :param user: The user to login :param remember: Flag specifying if the remember cookie should be set. If ``None`` use value of :py:data:`SECURITY_DEFAULT_REMEMBER_ME` :param authn_via: A list of strings denoting which mechanism(s) the user authenticated with. These should be one or more of ["password", "sms", "authenticator", "email"] or other 'auto-login' mechanisms. :return: True if user successfully logged in. """ if remember is None: remember = _config_value("DEFAULT_REMEMBER_ME") if not _login_user(user, remember, force=True): # pragma: no cover return False if _security.trackable: remote_addr = request.remote_addr or None # make sure it is None old_current_login, new_current_login = ( user.current_login_at, _security.datetime_factory(), ) old_current_ip, new_current_ip = user.current_login_ip, remote_addr user.last_login_at = old_current_login or new_current_login user.current_login_at = new_current_login user.last_login_ip = old_current_ip user.current_login_ip = new_current_ip user.login_count = user.login_count + 1 if user.login_count else 1 _datastore.put(user) session["fs_cc"] = "set" # CSRF cookie session["fs_paa"] = time.time() # Primary authentication at - timestamp identity_changed.send( current_app._get_current_object(), # type: ignore[attr-defined] _async_wrapper=current_app.ensure_sync, # type: ignore[arg-type] identity=Identity(user.fs_uniquifier), ) user_authenticated.send( current_app._get_current_object(), # type: ignore[attr-defined] _async_wrapper=current_app.ensure_sync, # type: ignore[arg-type] user=user, authn_via=authn_via, ) return True def logout_user() -> None: """Logs out the current user. This will also clean up the remember me cookie if it exists. This sends an ``identity_changed`` signal to note that the current identity is now the `AnonymousIdentity` """ for key in ( "identity.name", "identity.auth_type", "fs_paa", "fs_gexp", "fs_oauth_next", ): session.pop(key, None) # Clear csrf token between sessions. # Ideally this would be handled by Flask-WTF but... # We don't clear entire session since Flask-Login seems to like having it. csrf_field_name = _find_csrf_field_name() if csrf_field_name: session.pop(csrf_field_name, None) # Flask-WTF 'caches' csrf_token - and only set the session if not already # in 'g'. Be sure to clear both. This affects at least /confirm g.pop(csrf_field_name, None) session["fs_cc"] = "clear" if _config_value("REFRESH_TOKEN") and _config_value("REFRESH_TOKEN_COOKIE_NAME"): from .tokens import clear_refresh_token_cookie after_this_request(partial(clear_refresh_token_cookie)) identity_changed.send( current_app._get_current_object(), # type: ignore _async_wrapper=current_app.ensure_sync, identity=AnonymousIdentity(), ) _logout_user() def check_and_update_authn_fresh( within: timedelta, grace: timedelta, method: str | None = None, ) -> bool: """Check if user authenticated within specified time and update grace period. :param within: A timedelta specifying the maximum time in the past that the caller authenticated that is still considered 'fresh'. :param grace: A timedelta that, if the current session is considered 'fresh' will set a grace period for which freshness won't be checked. The intent here is that the caller shouldn't get part-way though a set of operations and suddenly be required to authenticate again. This is not supported for authentication tokens. :param method: Optional - if set and == "basic" then will always return True. (since basic-auth sends username/password on every request) If within.total_seconds() is negative, will always return True (always 'fresh'). This effectively just disables this entire mechanism. within.total_seconds() == 0 results in undefined behavior. If "fs_gexp" is in the session and the current timestamp is less than that, return True and extend grace time (i.e. set fs_gexp to current time + grace). Be aware that for this to work, state is required to be sent from the client. Flask security adds this state to the session (cookie) and the auth token. Without this state, 'False' is always returned - (not fresh). .. warning:: Be sure the caller is already authenticated PRIOR to calling this method. .. versionadded:: 3.4.0 .. versionchanged:: 4.0.0 Added `method` parameter. .. versionchanged:: 5.5.0 Grab 'Primary Authenticated At' from request_attrs which is set from either session or auth token """ if method == "basic": return True if within.total_seconds() < 0: # this means 'always fresh' return True if not (paa := get_request_attr("fs_paa")): # No recorded primary authenticated at time, you can't play. return False now = naive_utcnow() new_exp = now + grace grace_ts = int(new_exp.timestamp()) if fs_gexp := session.get("fs_gexp", None): if now.timestamp() < fs_gexp: # Within grace period - extend it, and we're good. session["fs_gexp"] = grace_ts return True authn_time = naive_utcfromtimestamp(paa) # allow for some time drift where it's possible authn_time is in the future # but let's be cautious and not allow arbitrary future times delta = now - authn_time if within > delta > -within: session["fs_gexp"] = grace_ts return True return False def get_hmac(password: str | bytes) -> bytes: """Returns a Base64 encoded HMAC+SHA512 of the password signed with the salt specified by :py:data:`SECURITY_PASSWORD_SALT`. :param password: The password to sign """ if not (salt := _config_value("PASSWORD_SALT")): raise RuntimeError( "The configuration value `SECURITY_PASSWORD_SALT` must " "not be None when the value of `SECURITY_PASSWORD_HASH` is " 'set to "%s"' % _config_value("PASSWORD_HASH") ) h = hmac.new(_encode_string(salt), _encode_string(password), hashlib.sha512) return base64.b64encode(h.digest()) def verify_password(password: str | bytes, password_hash: str | bytes) -> bool: """Returns ``True`` if the password matches the supplied hash. :param password: A plaintext password to verify :param password_hash: The expected hash value of the password (usually from your database) .. note:: Make sure that the password passed in has already been normalized. """ if _use_double_hash(password_hash): password = get_hmac(password) if _pwd_context.identify(password_hash) == "bcrypt": password = password[:72] return _pwd_context.verify(password, password_hash) def verify_and_update_password(password: str | bytes, user: UserMixin) -> bool: """Returns ``True`` if the password is valid for the specified user. Additionally, the hashed password in the database is updated if the hashing algorithm happens to have changed. N.B. you MUST call DB commit if you are using a session-based datastore (such as SqlAlchemy) since the user instance might have been altered (i.e. ``app.security.datastore.commit()``). This is usually handled in the view. :param password: A plaintext password to verify :param user: The user to verify against .. tip:: This should not be called directly - rather use :meth:`.UserMixin.verify_and_update_password` """ # Capture the original input in case we need to pass the unaltered # value to hash_password if the hashing algorithm has changed input_password = password if _use_double_hash(user.password): password = get_hmac(password) if _pwd_context.identify(user.password) == "bcrypt": password = password[:72] verified = _pwd_context.verify(password, user.password) else: # Try with original password. verified = _pwd_context.verify(password, user.password) if verified and (user.password is None or _pwd_context.needs_update(user.password)): user.password = hash_password(input_password) _datastore.put(user) return verified def hash_password(password: str | bytes) -> str: """Hash the specified plaintext password. Unless the hash algorithm (as specified by :py:data:`SECURITY_PASSWORD_HASH`) is listed in the configuration variable :py:data:`SECURITY_PASSWORD_SINGLE_HASH`, perform a double hash - first create an HMAC from the plaintext password and the value of :py:data:`SECURITY_PASSWORD_SALT`, then use the configured hashing algorithm. This satisfies OWASP/ASVS section 2.4.5: 'provide additional iteration of a key derivation'. .. versionadded:: 2.0.2 .. versionchanged:: 5.7.0 Explicit check for bcrypt truncation :param password: The plaintext password to hash """ if _use_double_hash(): password = get_hmac(password).decode("ascii") # Passing in options as part of hash is deprecated in passlib 1.7 # and new algorithms like argon2 don't even support it. if _config_value("PASSWORD_HASH") == "bcrypt": # bcrypt - OWASP says truncation concerns are negligible: # https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#input-limits-of-bcrypt password = password[:72] return _pwd_context.hash( password, **_config_value("PASSWORD_HASH_OPTIONS", default={}).get( _config_value("PASSWORD_HASH"), {} ), ) def _encode_string(string: t.Union[str, bytes]) -> bytes: """Encodes a string to bytes, if it isn't already. :param string: The string to encode :return: UTF-8 encoded bytes """ if isinstance(string, str): string = string.encode("utf-8") return string def hash_data(data: t.Union[str, bytes]) -> str: """Hashes input data after ensuring proper encoding. :param data: Input data to hash (will be encoded if not already bytes) :return: Hashed data as bytes Note: Uses application's configured _hashing_context """ return _hashing_context.hash(_encode_string(data)) def verify_hash(hashed_data: bytes, compare_data: t.Union[str, bytes]) -> bool: """Verifies data against a previously hashed value. :param hashed_data: Previously hashed data to compare against :param compare_data: Input data to verify (will be encoded if not already bytes) :return: True if data matches hash, False otherwise Note: Uses application's configured _hashing_context """ return _hashing_context.verify(_encode_string(compare_data), hashed_data) def _suppress_form_csrf(): """ Return meta contents if we should suppress form from attempting to validate CSRF. If app doesn't want CSRF for unauth endpoints then check if caller is authenticated or not (many endpoints can be called either way). """ if _config_value("CSRF_IGNORE_UNAUTH_ENDPOINTS") and not is_user_authenticated( current_user ): return {"csrf": False} return {} def _confirm_redirect(form, identity_attribute): """This is a very specific utility that all open endpoints call to implement the confirm redirect feature. """ if ( form.requires_confirmation and _config_value("REQUIRES_CONFIRMATION_ERROR_VIEW") and not _config_value("RETURN_GENERIC_RESPONSES") ): do_flash(*get_message("CONFIRMATION_REQUIRED")) return redirect( get_url( _config_value("REQUIRES_CONFIRMATION_ERROR_VIEW"), qparams={identity_attribute: getattr(form.user, identity_attribute)}, ) ) return None def do_flash(message: str, category: str) -> None: """Flash a message depending on if the `FLASH_MESSAGES` configuration value is set. :param message: The flash message :param category: The flash message category """ if _config_value("FLASH_MESSAGES"): flash(message, category) def parse_auth_token(auth_token: str) -> dict[str, t.Any]: """Parse an authentication token. This will raise an exception if not properly signed or expired """ tdata = dict() # This can raise BadSignature or SignatureExpired exceptions from itsdangerous raw_data = _security.remember_token_serializer.loads( auth_token, max_age=_config_value("TOKEN_MAX_AGE").total_seconds() ) # Version 3.x generated tokens that map to data with 3 elements, # and fs_uniquifier was on last element. # Version 4.0.0 generates tokens that map to data with only 1 element, # which maps to fs_uniquifier. # Version 5 and up are already a dict (with a version #) if isinstance(raw_data, dict): # new format - starting at ver=5 if not all(k in raw_data for k in ["ver", "uid", "exp"]): raise ValueError("Token missing keys") tdata = raw_data if ts := tdata.get("exp"): if ts < int(time.time()): raise SignatureExpired("token[exp] value expired") else: # old tokens that were lists if len(raw_data) == 1: # version 4 tdata["ver"] = "4" tdata["uid"] = raw_data[0] else: # version 3 tdata["ver"] = "3" tdata["uid"] = raw_data[2] return tdata def get_url(endpoint_or_url: str, qparams: dict[str, str] | None = None) -> str: """Returns a URL if a valid endpoint is found. Otherwise, returns the provided value. .. warning:: If an endpoint ISN'T provided, then it is assumed that the URL is external to Flask and if the spa configuration REDIRECT_HOST is set will redirect to that host. This could be an issue in development. :param endpoint_or_url: The endpoint name or URL to default to :param qparams: additional query params to add to end of url :return: URL """ try: return transform_url(url_for(endpoint_or_url), qparams) except Exception: # This is an external URL (no endpoint defined in app) # For (mostly) testing - allow changing/adding the url - for example # add a different host:port for cases where the UI is running # separately. if _config_value("REDIRECT_HOST"): url = transform_url( endpoint_or_url, qparams, netloc=_config_value("REDIRECT_HOST") ) else: url = transform_url(endpoint_or_url, qparams) return url def _slash_url_suffix(url: str, suffix: str) -> str: """ Formats a suffix to be appended to a URL, ensuring proper slash placement. If the given `url` ends with a slash, this function adds a trailing slash to the `suffix`. Otherwise, it adds a leading slash to the `suffix`. This helps prevent double slashes or missing slashes when constructing URLs. :param url: The base URL to which the suffix will be appended. :param suffix: The suffix to be appended to the URL. :return: The formatted suffix with the appropriate leading or trailing slash. Example: >>> _slash_url_suffix("https://example.com/api", "v1") '/v1' >>> _slash_url_suffix("https://example.com/api/", "v1") 'v1/' """ return url.endswith("/") and f"{suffix}/" or f"/{suffix}" def transform_url( url: str, qparams: dict[str, str] | None = None, **kwargs: str ) -> str: """Modify url :param url: url to transform (can be relative) :param qparams: additional query params to add to end of url :param kwargs: pieces of URL to modify - e.g. netloc=localhost:8000 :return: Modified URL .. versionadded:: 3.2.0 """ link_parse = urlsplit(url) if qparams: current_query = dict(parse_qsl(link_parse.query)) current_query.update(qparams) link_parse = link_parse._replace(query=urlencode(current_query)) return urlunsplit(link_parse._replace(**kwargs)) def _get_security_endpoint_name(endpoint: str) -> str: """ Returns the fully qualified endpoint name by combining the blueprint name and the endpoint. :param endpoint: The endpoint name to be combined with the blueprint name. :return: The fully qualified endpoint name in the format '.'. Example: >>> _get_security_endpoint_name("login") 'my_blueprint.login' """ return f"{_config_value('BLUEPRINT_NAME')}.{endpoint}" def url_for_security(endpoint: str, **values: t.Any) -> str: """Return a URL for the security blueprint :param endpoint: the endpoint of the URL (name of the function) :param values: the variable arguments of the URL rule :param _external: if set to `True`, an absolute URL is generated. Server address can be changed via `SERVER_NAME` configuration variable which defaults to `localhost`. :param _anchor: if provided, this is added as anchor to the URL. :param _method: if provided, this explicitly specifies an HTTP method. """ endpoint = _get_security_endpoint_name(endpoint) # mypy is complaining about this - but I think it's wrong? return url_for(endpoint, **values) # type: ignore def validate_redirect_url(url: str) -> bool: """Validate redirect URL In the default configuration only redirects to the same domain (and scheme) are allowed. The REDIRECT_ALLOW_SUBDOMAINS allows ANY subdomain of SERVER_NAME to be a redirect target. The REDIRECT_BASE_DOMAIN and REDIRECT_ALLOWED_SUBDOMAINS allow specifying 'side' redirects. """ if url is None or url.strip() == "": return False # Browsers treat backslashes like forward slashes, while urllib doesn't. # Since we just want to validate scheme and netloc here, we normalize # slashes to those recognized by urllib. We need to test both variants # though because replacing a backslash with a slash can result in different # ways to inject shit. for url_variant in (url, url.replace("\\", "/")): # Browsers are great at accepting crappy data, so `////example.com` is # handled like `//example.com`, even though it's invalid and `urlparse` # will not set a netloc when receiving such a URL. if url_variant.startswith("///"): return False url_info = urlsplit(url_variant) if url_info.scheme and url_info.scheme not in {"http", "https"}: return False if url_info.scheme and not url_info.netloc: return False url_next = urlsplit(url) url_base = urlsplit(request.host_url) if (url_next.netloc or url_next.scheme) and url_next.netloc != url_base.netloc: base_domain = current_app.config.get("SERVER_NAME") if ( _config_value("REDIRECT_ALLOW_SUBDOMAINS") and base_domain and ( url_next.netloc == base_domain or url_next.netloc.endswith(f".{base_domain}") ) ): return True base_domain = _config_value("REDIRECT_BASE_DOMAIN") if base_domain: allowable = [ base_domain if sub == "." else f"{sub}.{base_domain}" for sub in _config_value("REDIRECT_ALLOWED_SUBDOMAINS") ] return url_next.netloc in allowable return False return True def _get_post_action_redirect( config_key: str, next_loc: FlaskForm | MultiDict | dict | None ) -> str: """ There is a security angle here - the result of this method is sent to Flask::redirect() - and we need to be sure that it can't be interpreted as a user-input external URL - that would mean we would have an 'open-redirect' vulnerability. The complexity here is that urlsplit() does pretty well, but browsers even today May 2021 are very lenient in what they accept as URLs - for example: next=\\\\github.com next=%5C%5C%5Cgithub.com next=/////github.com next=%20\\\\github.com next=%20///github.com next=%20//github.com next=%19////github.com - i.e. browser will strip control chars next=%E2%80%8A///github.com - doesn't redirect! That is a unicode thin space. All will result in a null netloc and scheme from urlsplit - however many browsers will gladly strip off uninteresting characters and convert backslashes to forward slashes - and the cases above will actually cause a redirect to github.com Sigh. Some articles claim that a relative url has to start with a '/' - but that isn't strictly true. From: https://datatracker.ietf.org/doc/html/rfc3986#section-5 a relative path can start with a "//", "/", a non-colon, or be empty. So it seems that all the above URLs are valid. By the time we get the URL it may or may not have been unencoded - if part of a query string, then it has been, but if set in the form, not. This means we can't really determine if it is 'valid' since it appears that '/'s can appear in the URL if escaped. The solution is to simply 'quote' the path. netloc has a same issue: https://amazon.com\\.lp.com will cause many browsers to redirect to amazon.com """ rurl = _propagate_next(_find_redirect(config_key), next_loc) u = urlsplit(rurl) userinfo = "" if u.username or u.password: userinfo = f"{u.username}:{u.password}@" hostname = quote(u.hostname) if u.hostname else "" if u.port: netloc = f"{userinfo}{hostname}:{u.port}" else: netloc = f"{userinfo}{hostname}" safe_url = urlunsplit((u.scheme, netloc, quote(u.path), u.query, u.fragment)) return safe_url def _get_post_login_redirect() -> str: return _get_post_action_redirect("SECURITY_POST_LOGIN_VIEW", request.form) def _get_post_register_redirect() -> str: return _get_post_action_redirect("SECURITY_POST_REGISTER_VIEW", request.form) def _get_post_logout_redirect() -> str: return _get_post_action_redirect("SECURITY_POST_LOGOUT_VIEW", request.form) def _get_post_verify_redirect() -> str: return _get_post_action_redirect("SECURITY_POST_VERIFY_VIEW", request.form) def _find_redirect(key: str) -> str: """Returns the URL to redirect to. :param key: The application configuration key to search for """ app_url = None if app_value := current_app.config[key.upper()]: app_url = get_url(app_value) rv = app_url or str(current_app.config.get("APPLICATION_ROOT", "/")) return rv def _propagate_next( fallback_url: str, form: FlaskForm | MultiDict | dict | None ) -> str: """Compute appropriate redirect URL The application can add a 'next' query parameter or have 'next' as a form field. If either exist, make sure they are valid (not pointing to external location) If neither, return the fallback_url Can be passed either request.form (which is really a MultiDict OR a real form OR a dict with a 'next' key). """ form_next = None if form and isinstance(form, FlaskForm): if hasattr(form, "next") and form.next.data: form_next = form.next.data elif form and form.get("next", None): form_next = str(form.get("next")) arg_next = request.args.get("next") urls = [ get_url(form_next) if form_next else None, get_url(arg_next) if arg_next else None, fallback_url, ] for url in urls: if url and validate_redirect_url(url): return url raise ValueError("No valid redirect URL found - configuration error") def _simplify_url(base_url: str, redirect_url: str) -> str: """ Reduces the scheme and host from the redirect_url so it can be passed as a relative URL in a query (e.g. next) param. For this method we aren't worrying about a valid url (e.g. if it points externally) - that will be handled by later requests. :param base_url: The URL to simplify 'against'. :param redirect_url: The URL to reduce. """ b_url = urlsplit(base_url) r_url = urlsplit(redirect_url) if (not r_url.scheme or r_url.scheme == b_url.scheme) and ( not r_url.netloc or r_url.netloc == b_url.netloc ): return urlunsplit(("", "", r_url.path, r_url.query, r_url.fragment)) return redirect_url def get_message(key: str, **kwargs: t.Any) -> tuple[str, str]: rv = _config_value("MSG_" + key) return localize_callback(rv[0], **kwargs), rv[1] def _config_value(key, app=None, default=None, strict=True): """Get a Flask-Security configuration value. :param key: The configuration key without the prefix `SECURITY_` :param app: An optional specific application to inspect. Defaults to Flask's `current_app` :param default: An optional default value if the value is not set :param strict: if True, will raise ValueError if key doesn't exist """ app = app or current_app key = f"SECURITY_{key.upper()}" # protect against spelling mistakes if strict and key not in app.config: raise ValueError(f"Key {key} doesn't exist") return app.config.get(key, default) def _td_format(td: timedelta) -> str: """Formats a timedelta object to a string. Use humanize.precisedelta if available otherwise use this simplistic version of what humanize provides (English only). """ try: import humanize return humanize.precisedelta(td) except ImportError: periods = [ ("year", 60 * 60 * 24 * 365), ("month", 60 * 60 * 24 * 30), ("day", 60 * 60 * 24), ("hour", 60 * 60), ("minute", 60), ("second", 1), ] seconds = int(td.total_seconds()) strings = [] for period_name, period_seconds in periods: if seconds >= period_seconds: period_value, seconds = divmod(seconds, period_seconds) has_s = "s" if period_value > 1 else "" strings.append(f"{period_value} {period_name}{has_s}") if len(strings) == 2: return " and ".join(strings) return ", ".join(strings) def send_mail( subject: str, recipient: str | tuple[str, str], template: str, **context: t.Any ) -> None: """Send an email. :param subject: Email subject :param recipient: Email recipient :param template: The path/name of the email template :param context: The context to render the template with This formats the email and passes it off to :class:`.MailUtil` to actually send the message. """ context.setdefault("security", _security) context.update(_security._run_ctx_processor("mail")) body = None html = None if _config_value("EMAIL_PLAINTEXT"): body = _security.render_template(f"{template}.txt", **context) if _config_value("EMAIL_HTML"): html = _security.render_template(f"{template}.html", **context) subject = localize_callback(subject) sender = _config_value("EMAIL_SENDER") if isinstance(sender, LocalProxy): sender = sender._get_current_object() _security.mail_util.send_mail( template, subject, recipient, sender, body, html, **context, ) def check_and_get_token_status( token: str, serializer_name: str, within: timedelta ) -> tuple[bool, bool, t.Any]: """Get the status of a token and return data. :param token: The token to check :param serializer_name: The name of the serializer. Can be one of the following: ``confirm``, ``login``, ``reset``, ``us_setup`` ``remember``, ``tf_validity``, ``tf_setup``, ``wan``, ``change_email``, ``refresh_token`` :param within: max age - passed as a timedelta :return: a tuple of (expired, invalid, data) .. versionadded:: 3.4.0 """ serializer = getattr(_security, serializer_name + "_serializer") max_age = within.total_seconds() data = None expired, invalid = False, False try: data = serializer.loads(token, max_age=max_age) except SignatureExpired: d, data = serializer.loads_unsafe(token) expired = True except (BadSignature, TypeError, ValueError): invalid = True return expired, invalid, data def get_identity_attributes(app: Flask | None = None) -> list[str]: # Return list of keys of identity attributes # Is it possible to not have any? app = app or current_app iattrs = app.config["SECURITY_USER_IDENTITY_ATTRIBUTES"] if iattrs: return [[*f][0] for f in iattrs] return [] def get_identity_attribute(attr: str, app: Flask | None = None) -> dict[str, t.Any]: """Given a user_identity_attribute, return the defining dict. A bit annoying since USER_IDENTITY_ATTRIBUTES is a list of dict where each dict has just one key. """ app = app or current_app iattrs = app.config["SECURITY_USER_IDENTITY_ATTRIBUTES"] if iattrs: details = [ mapping[attr] for mapping in iattrs if list(mapping.keys())[0] == attr ] if details: return details[0] return {} def lookup_identity(identity: str | int) -> UserMixin | None: """ Lookup identity in DB. This loops through, in order, :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES`, and first calls the mapper function to normalize. Then the db.find_user is called on the specified user model attribute. """ for mapping in _config_value("USER_IDENTITY_ATTRIBUTES"): attr = list(mapping.keys())[0] details = mapping[attr] idata = details["mapper"](identity) if idata: user = _datastore.find_user( case_insensitive=details.get("case_insensitive", False), **{attr: idata} ) return user return None def uia_phone_mapper(identity: str) -> str | None: """Used to normalize a phone number. This is a simple proxy to :py:meth:`PhoneUtil.get_canonical_form` See :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES`. .. versionadded:: 3.4.0 """ ph = _security.phone_util.get_canonical_form(identity) return ph def uia_email_mapper(identity: str) -> str | None: """Used to normalize identity as an email. This is a simple proxy to :py:meth:`MailUtil.normalize` :return: Normalized email or None if not valid email. See :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES`. .. versionadded:: 3.4.0 """ try: return _security.mail_util.normalize(identity) except ValueError: return None def uia_username_mapper(identity: str) -> str | None: """Used to normalize a username. This is a simple proxy to :py:meth:`UsernameUtil.normalize` See :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES`. .. versionadded:: 4.1.0 """ return _security.username_util.normalize(identity) def _use_double_hash(password_hash=None): """Return a bool indicating whether a password should be hashed twice.""" # Default to plaintext for backward compatibility with # :py:data:`SECURITY_PASSWORD_SINGLE_HASH` = False single_hash = _config_value("PASSWORD_SINGLE_HASH") or {"plaintext"} if password_hash is None: scheme = _config_value("PASSWORD_HASH") else: scheme = _pwd_context.identify(password_hash) return not (single_hash is True or scheme in single_hash) def _csrf_cookie_handler(response: Response) -> Response: """Called at end of every request. Uses session to track state (set/clear) Ideally we just need to set this once - however by default Flask-WTF has a time-out on these tokens governed by *WTF_CSRF_TIME_LIMIT*. While we could set that to None - and OWASP implies this is fine - that might not be agreeable to everyone. So as a basic usability hack - we check if it is expired and re-generate so at least the user doesn't have to log out and back in (just refresh). We also support a *CSRF_COOKIE_REFRESH_EACH_REQUEST* analogous to Flask's *SESSION_REFRESH_EACH_REQUEST* It is of course removed on logout/session end. Other info on web suggests replacing on every POST and accepting up to 'age' ago. """ csrf_cookie = _config_value("CSRF_COOKIE") csrf_cookie_name = _config_value("CSRF_COOKIE_NAME") if not csrf_cookie_name: return response op = session.get("fs_cc", None) if not op: remember_cookie_name = current_app.config.get( "REMEMBER_COOKIE_NAME", REMEMBER_COOKIE_NAME ) has_remember_cookie = ( remember_cookie_name in request.cookies and session.get("remember") != "clear" ) # Set cookie if successfully logged in with flask_login's remember cookie if has_remember_cookie and is_user_authenticated(current_user): op = "set" else: return response if op == "clear": # Alas delete_cookie only accepts some of the keywords set_cookie does allowed = ["path", "domain", "secure", "httponly", "samesite"] args = {k: csrf_cookie.get(k) for k in allowed if k in csrf_cookie} response.delete_cookie(csrf_cookie_name, **args) session.pop("fs_cc") return response # Send a cookie if any of: # 1) CSRF_COOKIE_REFRESH_EACH_REQUEST is true # 2) fs_cc == "set" - this is on first login # 3) existing cookie has expired send = False if op == "set": send = True session["fs_cc"] = "sent" elif _config_value("CSRF_COOKIE_REFRESH_EACH_REQUEST"): send = True elif current_app.config["WTF_CSRF_TIME_LIMIT"]: current_cookie = request.cookies.get(csrf_cookie_name, None) if current_cookie: # Let's make sure it isn't expired if app doesn't set TIME_LIMIT to None. try: csrf.validate_csrf(current_cookie) except ValidationError: send = True if send: response.set_cookie(csrf_cookie_name, value=csrf.generate_csrf(), **csrf_cookie) return response def _add_cache_control(resp: Response) -> Response: """Add cache control header attributes to response.""" cc = _config_value("CACHE_CONTROL") or dict() for attr, v in cc.items(): resp.cache_control[attr] = v return resp def allowed_auth_token(user: UserMixin) -> bool: """When an endpoint could return an auth_token (such as all the authentication endpoints) - this utility if it should - based on configuration variables.""" if ( _config_value("BACKWARDS_COMPAT_AUTH_TOKEN") or "include_auth_token" in request.args ): return True return False def base_render_json( form: FlaskForm, include_user: bool = True, include_auth_token: bool = False, additional: dict[str, t.Any] | None = None, error_status_code: int = 400, ) -> ResponseValue: """ This method is called by all views that return JSON responses. This fills in the response and then calls :meth:`.Security.render_json` which can be overridden by the app. """ user = getattr(form, "user", None) if form.errors: code = error_status_code # wtforms 3.0 introduces form-level errors - these show up as part of the # errors dict with a key of 'None' payload = json_error_response(field_errors=form.errors) else: code = 200 payload = dict() if user: # This will be false for anonymous GETs via JSON from .tokens import response_tokens if include_user: payload["user"] = user.get_security_payload() if include_auth_token: response_tokens(user, payload) # Return csrf_token on each JSON response - just as every form # has it rendered. payload["csrf_token"] = csrf.generate_csrf() if additional: payload.update(additional) return _security._render_json(payload, code, None, user) def simple_render_json( additional: dict[str, t.Any] | None = None, ) -> ResponseValue: payload = dict(csrf_token=csrf.generate_csrf()) if additional: payload.update(additional) return _security._render_json(payload, 200, None, None) def _default_want_json(req): """Return True if response should be in json N.B. do not call this directly - use security._want_json() :param req: Flask/Werkzeug Request """ if req.is_json: return True # TODO should this handle json sub-types? accept_mimetypes = req.accept_mimetypes if not hasattr(req.accept_mimetypes, "best"): # pragma: no cover # Alright. we don't have the best property, lets add it ourselves. # This is for quart compatibility accept_mimetypes.best = best if accept_mimetypes.best == "application/json": return True return False def json_error_response( errors: str | list[str] | None = None, field_errors: dict[str, list[str]] | None = None, ) -> dict[str, t.Any]: """Helper to create an error response. The "errors" key holds a simple list of errors - which is made up of any passed errors (either a string or list) as well as the (localized) error msgs from the passed in field_errors. The "field_errors" key which is exactly what is returned from WTForms - namely a dict of field-name: [msg]. """ response_json: dict[str, list | dict[str, list]] = dict() plain_errors = [] if errors: if isinstance(errors, str): plain_errors = [errors] elif isinstance(errors, list): plain_errors = errors else: raise TypeError("The errors argument should be either a str or list.") if field_errors: # This is default from WTForms - a dictionary of field name and list of errors # we return that, as well as create a simple list of errors. for e in field_errors.values(): plain_errors.extend(e) response_json["field_errors"] = field_errors # type: ignore response_json["errors"] = plain_errors return response_json def _default_render_template(*args: t.Any, **kwargs: t.Any) -> str: return render_template(*args, **kwargs) class SmsSenderBaseClass(metaclass=abc.ABCMeta): @abc.abstractmethod def send_sms( self, from_number: str, to_number: str, msg: str ) -> None: # pragma: no cover """Abstract method for sending sms messages .. versionadded:: 3.2.0 """ return class DummySmsSender(SmsSenderBaseClass): def send_sms( self, from_number: str, to_number: str, msg: str ) -> None: # pragma: no cover """Do nothing.""" return None class SmsSenderFactory: senders: dict[str, t.Type[SmsSenderBaseClass]] = {"Dummy": DummySmsSender} @classmethod def createSender( cls, name: str, *args: t.Any, **kwargs: t.Any ) -> SmsSenderBaseClass: """Initialize an SMS sender. :param name: Name as registered in SmsSenderFactory:senders (e.g. 'Twilio') .. versionadded:: 3.2.0 """ return cls.senders[name](*args, **kwargs) try: # pragma: no cover from twilio.rest import Client class TwilioSmsSender(SmsSenderBaseClass): def __init__(self, *args: t.Any, **kwargs: t.Any): super().__init__(*args, **kwargs) self.account_sid: str = _config_value("SMS_SERVICE_CONFIG")["ACCOUNT_SID"] self.auth_token: str = _config_value("SMS_SERVICE_CONFIG")["AUTH_TOKEN"] def send_sms(self, from_number: str, to_number: str, msg: str) -> None: """Send message via twilio account.""" client = Client(self.account_sid, self.auth_token) client.messages.create(to=to_number, from_=from_number, body=msg) SmsSenderFactory.senders["Twilio"] = TwilioSmsSender except Exception: pass def password_length_validator(password: str) -> list[str] | None: """Test password for length. :param password: Plain text password to check :return: ``None`` if password conforms to length requirements, a list of error/suggestions if not. .. versionadded:: 3.4.0 """ if len(password) < _config_value("PASSWORD_LENGTH_MIN") or len(password) > 128: return [ get_message( "PASSWORD_INVALID_LENGTH", length=_config_value("PASSWORD_LENGTH_MIN") )[0] ] return None def password_complexity_validator( password: str, is_register: bool, **kwargs: t.Any ) -> list[str] | None: """Test password for complexity. Currently just supports 'zxcvbn'. :param password: Plain text password to check :param is_register: if True then kwargs are arbitrary additional info. (e.g. info from a registration form). If False, must be a SINGLE key "user" that corresponds to the current_user. All string values will be extracted and sent to the complexity checker. :param kwargs: :return: ``None`` if password is complex enough, a list of error/suggestions if not. Be aware that zxcvbn does not (easily) provide a way to localize messages. .. versionadded:: 3.4.0 """ if _config_value("PASSWORD_COMPLEXITY_CHECKER") == "zxcvbn": import zxcvbn user_info: list[t.Any] = [] if not is_register: for v in kwargs["user"].__dict__.values(): if v and isinstance(v, str): user_info.append(v) else: # This is usually all register form values that are in the user_model if kwargs: user_info = list(kwargs.values()) results = zxcvbn.zxcvbn(password, user_inputs=user_info) if results["score"] >= _config_value("ZXCVBN_MINIMUM_SCORE"): return None # Should we return suggestions? Default forms don't really know what to do. if results["feedback"]["warning"]: # Note that these come from zxcvbn and # aren't localizable via Flask-Security return [results["feedback"]["warning"]] return [get_message("PASSWORD_TOO_SIMPLE")[0]] else: return None def password_breached_validator(password: str) -> list[str] | None: """Check if password on breached list. Does nothing unless :py:data:`SECURITY_PASSWORD_CHECK_BREACHED` is set. If password is found on the breached list, return an error if the count is greater than or equal to :py:data:`SECURITY_PASSWORD_BREACHED_COUNT`. Uses :meth:`pwned`. :param password: Plain text password to check :return: ``None`` if password passes breached tests, else a list of error messages. .. versionadded:: 3.4.0 """ if pwn := _config_value("PASSWORD_CHECK_BREACHED"): try: cnt = pwned(password) if cnt >= _config_value("PASSWORD_BREACHED_COUNT"): return [get_message("PASSWORD_BREACHED")[0]] except Exception: if pwn == "strict": return [get_message("PASSWORD_BREACHED_SITE_ERROR")[0]] return None def pwned(password: str) -> int: """ Check password against pwnedpasswords API using k-Anonymity. https://haveibeenpwned.com/API/v3 :return: Count of password in DB (0 means hasn't been compromised) Can raise HTTPError .. versionadded:: 3.4.0 """ def convert_password_tuple(value): hash_suffix, count = value.split(":") return hash_suffix, int(count) sha1 = hashlib.sha1(password.encode("utf8")).hexdigest() req = urllib.request.Request( url=f"https://api.pwnedpasswords.com/range/{sha1[:5].upper()}", headers={"User-Agent": "Flask-Security (Python)"}, ) # Might raise HTTPError with urllib.request.urlopen(req) as f: response = f.read() raw = response.decode("utf-8-sig") entries = dict(map(convert_password_tuple, raw.upper().split("\r\n"))) return entries.get(sha1[5:].upper(), 0) def _handle_already_auth(form, payload=None): """ Allow already authenticated users. For GET this is useful for single-page-applications on refresh - session still active but need to access user info and csrf-token. For GET with forms - redirect to POST_LOGIN_VIEW. For POST - redirects to POST_LOGIN_VIEW (forms) or returns 400 (json). This does NOT use get_post_login_redirect() so that it doesn't look at 'next' - which can cause infinite redirect loops (see test_basic::test_authenticated_loop) While it's tempting to try to logout the current user and login the new requested user - that simply doesn't work with CSRF. """ if _security._want_json(request): if request.method == "POST": payload = json_error_response( errors=get_message("ANONYMOUS_USER_REQUIRED")[0] ) return _security._render_json(payload, 400, None, None) else: form.user = current_user return base_render_json(form, additional=payload) else: return redirect(get_url(_config_value("POST_LOGIN_VIEW"))) def input_svn( value: str, allowed_categories: list[str] | None, normalize_form: t.Literal["NFC", "NFD", "NFKC", "NFKD"] | None, ) -> t.Tuple[t.Literal["ok", "illegal", "unallowed"], str | None]: """ Used to sanitize, validate, and normalize (user/form) input allowed_categories: list of allowed categories (input to unicodedata.category()). normalize_form: input to unicodedata.normalize(). Returns a tuple of ("reason", cleaned_input) If cleaned_input is None, "reason" will be why. The "unallowed" response is dependent on the input 'allowed_categories' which normally is from a config variable. .. versionadded:: 5.9.0 """ import nh3 sanitized = nh3.clean(value.strip(), tags=set()) if sanitized != value.strip(): return "illegal", None if allowed_categories: cats = [unicodedata.category(c)[0] for c in sanitized] if any([cat not in allowed_categories for cat in cats]): return "unallowed", None if normalize_form: return "ok", unicodedata.normalize(normalize_form, sanitized) return "ok", sanitized pallets-eco-flask-security-72bf929/flask_security/views.py000066400000000000000000001526401525721743500240140ustar00rootroot00000000000000""" flask_security.views ~~~~~~~~~~~~~~~~~~~~ Flask-Security views module :copyright: (c) 2012 by Matt Wright. :copyright: (c) 2019-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. CSRF is tricky. By default all our forms have CSRF protection built in via Flask-WTF. This is regardless of authentication method or whether the request is Form or JSON based. Form-based 'just works' since when rendering the form (on GET), the CSRF token is automatically populated. We want to handle: - JSON requests where CSRF token is in a header (e.g. X-CSRF-Token) - Option to skip CSRF when using a token to authenticate (rather than session) (CSRF_PROTECT_MECHANISMS) - Option to skip CSRF for 'login'/unauthenticated requests (CSRF_IGNORE_UNAUTH_ENDPOINTS) This is complicated by the fact that the only way to disable form CSRF is to pass in meta={csrf: false} at form instantiation time. Be aware that for CSRF to work, caller MUST pass in session cookie. So for pure API, and no session cookie - there is no way to support CSRF-Login so app must set CSRF_IGNORE_UNAUTH_ENDPOINTS (or use CSRF/session cookie for logging in then once they have a token, no need for cookie). """ from __future__ import annotations from functools import partial import time import typing as t from flask import ( Blueprint, after_this_request, current_app, jsonify, request, session, ) from flask_login import current_user from flask_wtf.csrf import CSRFError from .changeable import change_user_password from .change_email import change_email, change_email_confirm from .change_username import change_username from .confirmable import ( confirm_email_token_status, confirm_user, send_confirmation_instructions, ) from .decorators import anonymous_user_required, auth_required, unauth_csrf, handle_csrf from .forms import ( _setup_methods_xlate, ChangePasswordForm, DummyForm, ForgotPasswordForm, LoginForm, LogoutForm, _build_form_from_request, _build_form, form_errors_munge, PasswordlessLoginForm, ResetPasswordForm, SendConfirmationForm, TwoFactorVerifyCodeForm, TwoFactorSetupForm, TwoFactorRescueForm, UsernameRecoveryForm, VerifyForm, ) from .passwordless import login_token_status, send_login_instructions from .proxies import _security, _datastore from .quart_compat import get_quart_status from .signals import tf_profile_changed from .tokens import refresh from .unified_signin import ( us_signin, us_signin_send_code, us_setup, us_setup_validate, us_verify, us_verify_link, us_verify_send_code, ) from .recoverable import ( send_reset_password_instructions, update_password, send_username_recovery_email, reset_password_token_status, ) from .registerable import register_user, register_existing from .recovery_codes import mf_recovery, mf_recovery_codes from .tf_plugin import ( tf_check_state, tf_illegal_state, tf_set_validity_token_cookie, ) from .twofactor import ( _complete_two_factor_process, _set_rescue_options, tf_clean_session, tf_disable, ) from .utils import ( base_render_json, check_and_update_authn_fresh, check_and_get_token_status, _config_value as cv, _confirm_redirect, do_flash, get_identity_attributes, get_message, _get_post_login_redirect, _get_post_logout_redirect, _get_post_register_redirect, _get_post_verify_redirect, get_request_attr, get_url, _handle_already_auth, hash_password, is_user_authenticated, localize_callback, login_user, logout_user, _propagate_next, send_mail, _slash_url_suffix, _td_format, url_for_security, _view_commit, allowed_auth_token, set_request_attr, ) from .webauthn import ( has_webauthn, webauthn_delete, webauthn_register, webauthn_register_response, webauthn_signin, webauthn_signin_response, webauthn_verify, webauthn_verify_response, ) if get_quart_status(): # pragma: no cover from quart import make_response, redirect else: from flask import make_response, redirect if t.TYPE_CHECKING: # pragma: no cover from flask.typing import ResponseReturnValue, ResponseValue from flask_security import UserMixin def default_render_json( payload: dict[str, t.Any], code: int, headers: dict[str, str] | None, user: UserMixin | None, ) -> ResponseValue: """Default JSON response handler.""" # Force Content-Type header to json. if headers is None: headers = dict() headers["Content-Type"] = "application/json" payload = dict(meta=dict(code=code), response=payload) return make_response(jsonify(payload), code, headers) def _ctx(endpoint): return _security._run_ctx_processor(endpoint) @unauth_csrf() def login() -> ResponseReturnValue: """View function for login view""" form = t.cast(LoginForm, _build_form_from_request("login_form")) if is_user_authenticated(current_user): return _handle_already_auth( form, payload={"identity_attributes": get_identity_attributes()} ) # Clean out any potential old session info - in case of previous # aborted 2FA attempt. tf_clean_session() if form.validate_on_submit(): assert form.user is not None remember_me = form.remember.data if "remember" in form else None response = _security.two_factor_plugins.tf_enter( form.user, remember_me, "password", next_loc=_propagate_next(request.url, form), ) if response: return response # two factor not required - login user after_this_request(_view_commit) login_user(form.user, remember=remember_me, authn_via=["password"]) if _security._want_json(request): return base_render_json( form, include_auth_token=allowed_auth_token(form.user), additional=dict(tf_required=False), ) return redirect(_get_post_login_redirect()) if request.method == "POST" and cv("RETURN_GENERIC_RESPONSES"): # Validation failed - make sure PII error messages are generic fields_to_squash = dict( email=dict(replace_msg="GENERIC_AUTHN_FAILED"), password=dict(replace_msg="GENERIC_AUTHN_FAILED"), ) if hasattr(form, "username"): fields_to_squash["username"] = dict(replace_msg="GENERIC_AUTHN_FAILED") form_errors_munge(form, fields_to_squash) if request.method == "GET": # set CSRF COOKIE if configured. This is the equivalent of forms and # base_render_json always sending the csrf_token session["fs_cc"] = "set" if _security._want_json(request): payload = { "identity_attributes": get_identity_attributes(), } return base_render_json(form, additional=payload) if rurl := _confirm_redirect(form, "email"): return rurl return _security.render_template( cv("LOGIN_USER_TEMPLATE"), login_user_form=form, identity_attributes=get_identity_attributes(), **_ctx("login"), ) @auth_required(lambda: cv("API_ENABLED_METHODS")) def verify() -> ResponseReturnValue: """View function which handles a reauthentication request.""" form = t.cast( VerifyForm, _build_form_from_request("verify_form", user=current_user) ) if form.validate_on_submit(): # form may have called verify_and_update_password() after_this_request(_view_commit) # verified - so set freshness time. session["fs_paa"] = time.time() if _security._want_json(request): return base_render_json( form, include_auth_token=allowed_auth_token(form.user) ) do_flash(*get_message("REAUTHENTICATION_SUCCESSFUL")) return redirect(_get_post_verify_redirect()) webauthn_available = has_webauthn(current_user, cv("WAN_ALLOW_AS_VERIFY")) if _security._want_json(request): provider_names: list[str] = [] if cv("OAUTH_ENABLE"): assert _security.oauthglue is not None provider_names = _security.oauthglue.provider_names payload = { "has_webauthn_verify_credential": webauthn_available, "oauth_enabled": cv("OAUTH_ENABLE"), "oauth_providers": provider_names, } return base_render_json(form, additional=payload) return _security.render_template( cv("VERIFY_TEMPLATE"), verify_form=form, has_webauthn_verify_credential=webauthn_available, wan_verify_form=_build_form("wan_verify_form"), **_ctx("verify"), ) def logout() -> ResponseReturnValue: """View function which handles a logout request. As part of the refresh_token feature, logout now has a form defined which a client can pass a refresh token that will be revoked as part of logout (if the refresh token is managed with a cookie, the value will be set into the form). The cookie AND refresh tracker/token will be revoked. If CSRF is configured and fails - a confirmation dialog will be shown (which has the csrf token populated). """ tf_clean_session() if is_user_authenticated(current_user): form = t.cast( LogoutForm, _build_form_from_request("logout_form", user=current_user) ) if cv("LOGOUT_CSRF"): try: # if failed and JSON - return 400 response if response := handle_csrf( get_request_attr("fs_authn_via"), _security._want_json(request) ): return response except CSRFError: return _security.render_template( cv("LOGOUT_USER_TEMPLATE"), logout_form=form, **_ctx("logout"), ) else: # ensure logout succeeds set_request_attr("csrf_valid", True) if form.validate_on_submit(): # if they passed a refresh token - revoke it from .tokens import _revoke_refresh_tracker if form.refresh_tracker and not form.refresh_tracker.revoked_at: _revoke_refresh_tracker( form.refresh_tracker, form.refresh_errors, current_user ) if cv("LOGOUT_CSRF"): # Could be a CSRF error from form validate csrf_field = getattr(form, form.meta.csrf_field_name) if csrf_field and csrf_field.errors: if _security._want_json(request): return base_render_json(form) return _security.render_template( cv("LOGOUT_USER_TEMPLATE"), logout_form=form, **_ctx("logout"), ) logout_user() # No body is required - so if a POST and json - return OK if request.method == "POST" and _security._want_json(request): return _security._render_json({}, 200, None, None) return redirect(_get_post_logout_redirect()) @anonymous_user_required @unauth_csrf() def register() -> ResponseReturnValue: """View function which handles a registration request.""" if (_security.confirmable or request.is_json) and _security.forms[ "confirm_register_form" ].cls: form_name = "confirm_register_form" else: form_name = "register_form" form = _build_form_from_request(form_name) if form.validate_on_submit(): after_this_request(_view_commit) user = register_user(form) form.user = user # The 'auto-login' feature probably should be removed - I can't imagine # an application that would want random email accounts. It has been like this # since the beginning. Note that we still enforce 2FA - however for unified # signin - we adhere to historic behavior. if not _security.confirmable or cv("LOGIN_WITHOUT_CONFIRMATION"): response = _security.two_factor_plugins.tf_enter( form.user, False, "register", next_loc=_propagate_next(request.url, form), ) if response: return response # two factor not required - login user. login_user(user, authn_via=["register"]) if _security._want_json(request): return base_render_json( form, include_auth_token=allowed_auth_token(form.user), additional=dict(tf_required=False), ) if not _security._want_json(request): return redirect(_get_post_register_redirect()) return base_render_json(form) # Here on GET or failed validate if request.method == "POST" and cv("RETURN_GENERIC_RESPONSES"): gr = register_existing(form) if gr: if _security._want_json(request): return base_render_json(form) return redirect(_get_post_register_redirect()) if _security._want_json(request): return base_render_json(form) return _security.render_template( cv("REGISTER_USER_TEMPLATE"), register_user_form=form, **_ctx("register"), ) @unauth_csrf() def send_login() -> ResponseReturnValue: """View function that sends login instructions for passwordless login""" form = t.cast( PasswordlessLoginForm, _build_form_from_request("passwordless_login_form") ) if form.validate_on_submit(): send_login_instructions(form.user) assert form.user if not _security._want_json(request): do_flash(*get_message("LOGIN_EMAIL_SENT", email=form.user.email)) if _security._want_json(request): return base_render_json(form) return _security.render_template( cv("SEND_LOGIN_TEMPLATE"), send_login_form=form, **_ctx("send_login") ) @anonymous_user_required def token_login(token: str) -> ResponseReturnValue: """View function that handles passwordless login via a token Like reset-password and confirm - this is usually a GET via an email so from the request we can't differentiate form-based apps from non. """ expired, invalid, user = login_token_status(token) if not user or invalid: m, c = get_message("INVALID_LOGIN_TOKEN") if cv("REDIRECT_BEHAVIOR") == "spa": return redirect(get_url(cv("LOGIN_ERROR_VIEW"), qparams={c: m})) do_flash(m, c) return redirect(url_for_security("login")) if expired: send_login_instructions(user) m, c = get_message( "LOGIN_EXPIRED", email=user.email, within=_td_format(cv("LOGIN_WITHIN")) ) if cv("REDIRECT_BEHAVIOR") == "spa": return redirect( get_url( cv("LOGIN_ERROR_VIEW"), qparams=user.get_redirect_qparams({c: m}), ) ) do_flash(m, c) return redirect(url_for_security("login")) login_user(user, authn_via=["token"]) after_this_request(_view_commit) if cv("REDIRECT_BEHAVIOR") == "spa": return redirect( get_url(cv("POST_LOGIN_VIEW"), qparams=user.get_redirect_qparams()) ) do_flash(*get_message("PASSWORDLESS_LOGIN_SUCCESSFUL")) return redirect(_get_post_login_redirect()) @unauth_csrf() def send_confirmation() -> ResponseReturnValue: """View function which sends confirmation instructions (/confirm).""" form = t.cast( SendConfirmationForm, _build_form_from_request("send_confirmation_form") ) if form.validate_on_submit(): send_confirmation_instructions(form.user) if not _security._want_json(request): do_flash(*get_message("CONFIRMATION_REQUEST", email=form.email.data)) elif request.method == "POST" and cv("RETURN_GENERIC_RESPONSES"): # Here on GET or failed validate rinfo: dict[str, dict[str, str]] = dict(email=dict()) form_errors_munge(form, rinfo) # by suppressing errors JSON should return 200 # Check for other errors - for default form - there aren't additional fields # but applications might add some (e.g. recaptcha) if not form.errors: # Make look exactly like successful (e.g. real user) request if not _security._want_json(request): do_flash(*get_message("CONFIRMATION_REQUEST", email=form.email.data)) if _security._want_json(request): # Never include user info since this is an anonymous endpoint. return base_render_json(form, include_user=False) return _security.render_template( cv("SEND_CONFIRMATION_TEMPLATE"), send_confirmation_form=form, **_ctx("send_confirmation"), ) def confirm_email(token: str) -> ResponseReturnValue: """ View function which handles an email confirmation request. This is always a GET from an email - so for 'spa' must always redirect. """ expired, invalid, user = confirm_email_token_status(token) if not user or invalid or expired: if expired: m, c = get_message( "CONFIRMATION_EXPIRED", within=_td_format(cv("CONFIRM_EMAIL_WITHIN")), ) else: m, c = get_message("INVALID_CONFIRMATION_TOKEN") if cv("REDIRECT_BEHAVIOR") == "spa": return redirect(get_url(cv("CONFIRM_ERROR_VIEW"), qparams={c: m})) do_flash(m, c) return redirect( get_url(cv("CONFIRM_ERROR_VIEW")) or url_for_security("send_confirmation") ) already_confirmed = user.confirmed_at is not None if already_confirmed: m, c = get_message("ALREADY_CONFIRMED") if cv("REDIRECT_BEHAVIOR") == "spa": # No reason to expose identity info to anyone who has the link return redirect( get_url( cv("CONFIRM_ERROR_VIEW"), qparams={c: m}, ) ) do_flash(m, c) return redirect( get_url(cv("CONFIRM_ERROR_VIEW")) or url_for_security("send_confirmation") ) confirm_user(user) after_this_request(_view_commit) m, c = get_message("EMAIL_CONFIRMED") # ? The only case where user is logged in already would be if # LOGIN_WITHOUT_CONFIRMATION if user != current_user: logout_user() if cv("AUTO_LOGIN_AFTER_CONFIRM"): # N.B. this is a (small) security risk if email went to wrong place. # and you have the LOGIN_WITHOUT_CONFIRMATION flag since in that case # you can be logged in and doing stuff - but another person could # get the email. # Note also this goes against OWASP recommendations. response = _security.two_factor_plugins.tf_enter( user, False, "confirm", next_loc=_propagate_next(request.url, None) ) if response: do_flash(m, c) return response login_user(user, authn_via=["confirm"]) if cv("REDIRECT_BEHAVIOR") == "spa": return redirect( get_url( cv("POST_CONFIRM_VIEW"), qparams=user.get_redirect_qparams({c: m}), ) ) do_flash(m, c) return redirect( get_url(cv("POST_CONFIRM_VIEW")) or get_url( cv("POST_LOGIN_VIEW") if cv("AUTO_LOGIN_AFTER_CONFIRM") else ".login" ) ) @unauth_csrf() def forgot_password() -> ResponseReturnValue: """View function that handles a forgotten password request (/reset). This is allowed for either anonymous or authenticated users. The rationale is that often users stay logged in for a long time and might have forgotten their password and might be prompted for it for sensitive operations (/verify). """ form = t.cast(ForgotPasswordForm, _build_form_from_request("forgot_password_form")) if form.validate_on_submit(): send_reset_password_instructions(form.user) if not _security._want_json(request): do_flash(*get_message("PASSWORD_RESET_REQUEST", email=form.email.data)) elif request.method == "POST" and cv("RETURN_GENERIC_RESPONSES"): # Here on failed validate (POST) and want generic responses rinfo: dict[str, dict[str, str]] = dict(email=dict()) form_errors_munge(form, rinfo) # by suppressing errors JSON should return 200 # Check for other errors - for default form - there aren't additional fields # but applications might add some (e.g. recaptcha) if not form.errors: # No OTHER errors on form. # Make look exactly like successful (e.g. real user) request hash_password("not-a-password") # reduce timing between successful and not. if not _security._want_json(request): do_flash(*get_message("PASSWORD_RESET_REQUEST", email=form.email.data)) if _security._want_json(request): # Never include user info since this is an anonymous endpoint. return base_render_json(form, include_user=False) if rurl := _confirm_redirect(form, "email"): return rurl if is_user_authenticated(current_user): form.email.data = current_user.email return _security.render_template( cv("FORGOT_PASSWORD_TEMPLATE"), forgot_password_form=form, **_ctx("forgot_password"), ) @unauth_csrf() def reset_password(token: str) -> ResponseReturnValue: """View function that handles a reset password request (/reset/). This endpoint can be called either when authenticated or anonymous This is usually called via GET as part of an email link and redirects to a reset-password form. It is called via POST to actually update the password (and then redirects to a post reset/login view) If in either case the token is either invalid or expired it redirects to the 'forgot-password' form. In the case of non-form based configuration: For GET normal case - redirect to RESET_VIEW?token={token} For GET invalid case - redirect to RESET_ERROR_VIEW?error={error} For POST normal/successful case - return 200 with new authentication token For POST error case return 400 """ form = t.cast(ResetPasswordForm, _build_form_from_request("reset_password_form")) expired, invalid, form.user = reset_password_token_status(token) if request.method == "GET": if not form.user or invalid or expired: if expired: m, c = get_message( "PASSWORD_RESET_EXPIRED", within=_td_format(cv("RESET_PASSWORD_WITHIN")), ) else: m, c = get_message("INVALID_RESET_PASSWORD_TOKEN") if cv("REDIRECT_BEHAVIOR") == "spa": return redirect(get_url(cv("RESET_ERROR_VIEW"), qparams={c: m})) do_flash(m, c) return redirect(url_for_security("forgot_password")) # All good - for SPA - redirect to the ``reset_view`` # Still - don't include PII such as identity and email if someone # intercepts link they still won't necessarily know the login identity # (even though they can change the password!). if cv("REDIRECT_BEHAVIOR") == "spa": return redirect( get_url( cv("RESET_VIEW"), qparams={"token": token}, ) ) # for forms - render the reset password form return _security.render_template( cv("RESET_PASSWORD_TEMPLATE"), reset_password_form=form, reset_password_token=token, **_ctx("reset_password"), ) # This is the POST case. if not form.user or invalid or expired: if expired: m, c = get_message( "PASSWORD_RESET_EXPIRED", within=_td_format(cv("RESET_PASSWORD_WITHIN")) ) else: m, c = get_message("INVALID_RESET_PASSWORD_TOKEN") if _security._want_json(request): form.form_errors.append(m) return base_render_json(form, include_user=False) else: do_flash(m, c) return redirect(url_for_security("forgot_password")) if form.validate_on_submit(): after_this_request(_view_commit) update_password(form.user, form.password.data) if cv("AUTO_LOGIN_AFTER_RESET"): # backwards compat - really shouldn't do this according to OWASP response = _security.two_factor_plugins.tf_enter( form.user, False, "reset", next_loc=_propagate_next(request.url, None) ) if response: return response # two factor not required - just login login_user(form.user, authn_via=["reset"]) if _security._want_json(request): dummy_form = DummyForm(formdata=None) dummy_form.user = form.user return base_render_json( dummy_form, include_auth_token=allowed_auth_token(form.user), additional=dict(tf_required=False), ) else: do_flash(*get_message("PASSWORD_RESET")) return redirect( get_url(cv("POST_RESET_VIEW")) or get_url(cv("POST_LOGIN_VIEW")) ) else: if _security._want_json(request): return _security._render_json({}, 200, None, None) else: do_flash(*get_message("PASSWORD_RESET_NO_LOGIN")) return redirect(get_url(cv("POST_RESET_VIEW")) or get_url(".login")) # validation failure case - for forms - we try again including the token # for non-forms - we just return errors and assume caller remembers token. if _security._want_json(request): return base_render_json(form) return _security.render_template( cv("RESET_PASSWORD_TEMPLATE"), reset_password_form=form, reset_password_token=token, **_ctx("reset_password"), ) @auth_required(lambda: cv("API_ENABLED_METHODS")) def change_password() -> ResponseReturnValue: """View function which handles a change password request.""" form = t.cast(ChangePasswordForm, _build_form_from_request("change_password_form")) if not current_user.password: # This is case where user registered w/o a password - since we can't # confirm with existing password - make sure fresh using whatever authentication # method they have set up. if not check_and_update_authn_fresh( cv("FRESHNESS"), cv("FRESHNESS_GRACE_PERIOD"), get_request_attr("fs_authn_via"), ): return _security._reauthn_handler( cv("FRESHNESS"), cv("FRESHNESS_GRACE_PERIOD") ) if form.validate_on_submit(): after_this_request(_view_commit) change_user_password(current_user._get_current_object(), form.new_password.data) if _security._want_json(request): form.user = current_user return base_render_json( form, include_auth_token=allowed_auth_token(form.user) ) do_flash(*get_message("PASSWORD_CHANGE")) return redirect( get_url(cv("POST_CHANGE_VIEW")) or get_url(cv("POST_LOGIN_VIEW")) ) active_password = True if current_user.password else False if _security._want_json(request): form.user = current_user payload = dict(active_password=active_password) return base_render_json(form, additional=payload) return _security.render_template( cv("CHANGE_PASSWORD_TEMPLATE"), change_password_form=form, active_password=active_password, **_ctx("change_password"), ) @unauth_csrf() def two_factor_setup() -> ResponseReturnValue: """View function for two-factor setup. This is used both for GET to fetch forms and POST to actually set configuration (and send token). There are 3 cases for setting up: 1) initial login and application requires 2FA 2) changing existing 2FA information 3) user wanting to enable or disable 2FA (assuming application doesn't require it) In order to CHANGE/ENABLE/DISABLE a 2FA information, user must be properly logged in AND have a 'fresh' authentication. For initial login when 2FA required of course user can't be logged in - in this case we need to have been sent some state via the session as part of login to show a) who and b) that they successfully authenticated. """ form = t.cast(TwoFactorSetupForm, _build_form_from_request("two_factor_setup_form")) changing = is_user_authenticated(current_user) if not changing: # This is the initial login case if not all(k in session for k in ["tf_user_id", "tf_state"]) or session[ "tf_state" ] not in ["setup_from_login", "validating_profile"]: # illegal call on this endpoint tf_clean_session() return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW")) user = _datastore.find_user(fs_uniquifier=session["tf_user_id"]) if not user: tf_clean_session() return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW")) else: # Caller is changing their TFA profile. This requires a 'fresh' authentication # N.B unauth_csrf has done the CSRF check already. if not check_and_update_authn_fresh( cv("FRESHNESS"), cv("FRESHNESS_GRACE_PERIOD"), get_request_attr("fs_authn_via"), ): return _security._reauthn_handler( cv("FRESHNESS"), cv("FRESHNESS_GRACE_PERIOD") ) user = current_user form.user = user if form.validate_on_submit(): # Before storing in DB and therefore requiring 2FA we need to # make sure it actually works. # Requiring 2FA is triggered by having: # - BOTH tf_totp_secret and tf_primary_method in the user record # - OR having the application global config TWO_FACTOR_REQUIRED # - OR User.check_tf_required() returns True (overridden by app). # Until we correctly validate the 2FA - we don't set primary_method in # user model but use the session to store it. pm = form.setup.data if pm == "disable": tf_disable(user) after_this_request(_view_commit) if not _security._want_json(request): do_flash(*get_message("TWO_FACTOR_DISABLED")) return redirect(get_url(cv("TWO_FACTOR_POST_SETUP_VIEW"))) else: return base_render_json(form) # Regenerate the TOTP secret on every call of 2FA setup totp = _security.totp_factory.generate_totp_secret() phone = form.phone.data if pm == "sms" else None session["tf_totp_secret"] = totp session["tf_primary_method"] = pm session["tf_state"] = "validating_profile" # currently - state_token only works for changing TFA - not initial login state_token = None if changing: state = { "totp_secret": totp, "method": pm, "phone": phone, } state_token = _security.tf_setup_serializer.dumps(state) json_response = { "tf_state": "validating_profile", # deprecated in 5.5.0 "tf_primary_method": pm, # old "tf_method": pm, "tf_state_token": state_token, } if phone: # TODO dont save here - wait until complete user.tf_phone_number = phone _datastore.put(user) after_this_request(_view_commit) if ( pm == "email" or pm == "sms" ): # TODO not sure this is needed - send checks this msg = user.tf_send_security_token( method=pm, totp_secret=totp, phone_number=phone, ) if msg: # send code didn't work form.setup.errors = list() form.setup.errors.append(msg) if _security._want_json(request): return base_render_json( form, include_user=False, error_status_code=500 ) qrcode_values = dict() if pm == "authenticator": authr_setup_values = _security.totp_factory.fetch_setup_values(totp, user) # Add all the values used in qrcode to json response json_response["tf_authr_key"] = authr_setup_values["key"] json_response["tf_authr_b32key"] = authr_setup_values["b32key"] json_response["tf_authr_username"] = authr_setup_values["username"] json_response["tf_authr_issuer"] = authr_setup_values["issuer"] json_response["tf_authr_uri"] = authr_setup_values["uri"] qrcode_values = dict( authr_qrcode=authr_setup_values["image"], authr_key=authr_setup_values["key"], authr_username=authr_setup_values["username"], authr_issuer=authr_setup_values["issuer"], ) if _security._want_json(request): return base_render_json(form, include_user=False, additional=json_response) code_form = _build_form("two_factor_verify_code_form") return _security.render_template( cv("TWO_FACTOR_SETUP_TEMPLATE"), two_factor_setup_form=form, two_factor_verify_code_form=code_form, choices=cv("TWO_FACTOR_ENABLED_METHODS"), chosen_method=pm, # do not translate primary_method=localize_callback( _setup_methods_xlate[getattr(user, "tf_primary_method", None)] ), changing=changing, state_token=state_token, **qrcode_values, **_ctx("tf_setup"), ) # We get here on GET and POST with failed validation. choices = cv("TWO_FACTOR_ENABLED_METHODS")[:] tf_required = user.check_tf_required_setup() if (not tf_required) and user.tf_primary_method is not None: choices.insert(0, "disable") if _security._want_json(request): # Provide information application/UI might need to render their own form/input json_response = { "tf_required": tf_required, "tf_primary_method": getattr(user, "tf_primary_method", None), # old "tf_method": getattr(user, "tf_primary_method", None), "tf_phone_number": getattr(user, "tf_phone_number", None), "tf_available_methods": choices, } return base_render_json(form, include_user=False, additional=json_response) code_form = _build_form("two_factor_verify_code_form") return _security.render_template( cv("TWO_FACTOR_SETUP_TEMPLATE"), two_factor_setup_form=form, two_factor_verify_code_form=code_form, choices=choices, chosen_method=None, primary_method=localize_callback( _setup_methods_xlate[getattr(user, "tf_primary_method", None)] ), changing=changing, state_token=None, two_factor_required=tf_required, **_ctx("tf_setup"), ) @auth_required(lambda: cv("API_ENABLED_METHODS")) def two_factor_setup_validate(token: str) -> ResponseReturnValue: """ Validate new setup. The token is the state variable that is signed and timed and contains all the state that once confirmed will be stored in the user record. Unlike the code in two_factor_token_validation - this works w/o a session. It also is JUST for setting up/changing two factor for an authenticated user. """ form = t.cast( TwoFactorVerifyCodeForm, _build_form_from_request("two_factor_verify_code_form") ) expired, invalid, state = check_and_get_token_status( token, "tf_setup", cv("TWO_FACTOR_SETUP_WITHIN") ) if invalid: m, c = get_message("API_ERROR") if expired: m, c = get_message( "TWO_FACTOR_SETUP_EXPIRED", within=_td_format(cv("TWO_FACTOR_SETUP_WITHIN")) ) if invalid or expired: tf_clean_session() # until we completely remove session based setup/state if _security._want_json(request): form.form_errors.append(m) return base_render_json(form, include_user=False) do_flash(m, c) return redirect(url_for_security("two_factor_setup")) totp_secret = state["totp_secret"] method = state["method"] phone = state["phone"] form.tf_totp_secret = totp_secret form.primary_method = method form.user = current_user form.is_setup = True if form.validate_on_submit(): tf_clean_session() # until we completely remove session based setup/state after_this_request(_view_commit) _datastore.tf_set(current_user, method, totp_secret, phone) # TODO: should validity cookie be removed? extended? left alone? # Currently - leave it alone - meaning cookie still set. tf_profile_changed.send( current_app._get_current_object(), # type: ignore[attr-defined] _async_wrapper=current_app.ensure_sync, user=current_user, method=method, ) if _security._want_json(request): return base_render_json( form, include_user=False, additional=dict( tf_method=method, tf_primary_method=method, tf_phone=current_user.tf_phone_number, ), ) else: do_flash(*get_message("TWO_FACTOR_CHANGE_METHOD_SUCCESSFUL")) return redirect(get_url(cv("TWO_FACTOR_POST_SETUP_VIEW"))) # Code not correct/outdated. if _security._want_json(request): return base_render_json(form, include_user=False) m, c = get_message("TWO_FACTOR_INVALID_TOKEN") do_flash(m, c) return redirect(url_for_security("two_factor_setup")) @unauth_csrf() def two_factor_token_validation() -> ResponseReturnValue: """View function for two-factor token validation Two cases: 1) normal login case - everything setup correctly; normal 2FA validation In this case - user not logged in - but 'tf_state' == 'ready' or 'validating_profile' 2) validating after CHANGE/ENABLE 2FA. In this case user logged in/authenticated In this case we allow a GET to get the specific enter-code form. """ form = t.cast( TwoFactorVerifyCodeForm, _build_form_from_request("two_factor_verify_code_form") ) # state info in session pm = session.get("tf_primary_method", None) totp_secret = session.get("tf_totp_secret", None) tf_state = session.get("tf_state", None) tf_user_id = session.get("tf_user_id", None) changing = is_user_authenticated(current_user) if not changing: # This is the normal login case OR initial setup (two factor required) if ( tf_state not in ["ready", "validating_profile"] or (tf_state == "validating_profile" and not all([pm, totp_secret])) or not tf_user_id ): # illegal call on this endpoint tf_clean_session() return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW")) user = _datastore.find_user(fs_uniquifier=tf_user_id) form.user = user if not user: tf_clean_session() return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW")) if tf_state == "ready": # normal login case - use saved values pm = user.tf_primary_method totp_secret = user.tf_totp_secret else: # Changing TFA profile - user is already authenticated. if tf_state != "validating_profile" or not all([pm, totp_secret]): tf_clean_session() # logout since this seems like attack-ish/logic error logout_user() return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW")) form.user = current_user form.is_setup = True form.primary_method = pm form.tf_totp_secret = totp_secret assert form.user if form.validate_on_submit(): # Success - finish process based on 'changing' and clear all session variables completion_message, token = _complete_two_factor_process( form.user, pm, totp_secret, changing ) after_this_request(_view_commit) if token: after_this_request(partial(tf_set_validity_token_cookie, token=token)) if not _security._want_json(request): do_flash(*get_message(completion_message)) if changing: return redirect(get_url(cv("TWO_FACTOR_POST_SETUP_VIEW"))) else: return redirect(_get_post_login_redirect()) else: return base_render_json( form, include_auth_token=allowed_auth_token(form.user) ) # GET or not successful POST # if we were trying to validate a new method if changing: if _security._want_json(request): return base_render_json(form) # allow app to fetch just this form (independent of /tf_setup) return _security.render_template( cv("TWO_FACTOR_VERIFY_CODE_TEMPLATE"), two_factor_verify_code_form=form, chosen_method=localize_callback(_setup_methods_xlate[pm]), **_ctx("tf_token_validation"), ) # if we were trying to validate an existing method else: rescue_form = _build_form("two_factor_rescue_form") recovery_options = _set_rescue_options(rescue_form, form.user) if _security._want_json(request): return base_render_json( form, additional=dict(recovery_options=recovery_options) ) return _security.render_template( cv("TWO_FACTOR_VERIFY_CODE_TEMPLATE"), two_factor_rescue_form=rescue_form, two_factor_verify_code_form=form, chosen_method=localize_callback(_setup_methods_xlate[pm]), problem=None, **_ctx("tf_token_validation"), ) @anonymous_user_required @unauth_csrf() def two_factor_rescue() -> ResponseReturnValue: """Function that handles a situation where user can't enter his two-factor validation code User must have already provided valid username/password. User must have already established 2FA """ form = t.cast( TwoFactorRescueForm, _build_form_from_request("two_factor_rescue_form") ) form.user = tf_check_state(["ready"]) if not form.user: return tf_illegal_state(form, cv("TWO_FACTOR_ERROR_VIEW")) recovery_options = _set_rescue_options(form, form.user) rproblem = "" if form.validate_on_submit(): raction = form.help_setup.data rproblem = raction if raction == "email": msg = form.user.tf_send_security_token( method="email", totp_secret=form.user.tf_totp_secret, phone_number=getattr(form.user, "tf_phone_number", None), ) if msg: rproblem = "" assert isinstance(form.help_setup.errors, list) form.help_setup.errors.append(msg) if _security._want_json(request): return base_render_json( form, include_user=False, error_status_code=500 ) # drop through to GET path elif raction == "recovery_code": return redirect(url_for_security("mf_recovery")) # send app provider a mail message regarding trouble elif raction == "help": send_mail( cv("EMAIL_SUBJECT_TWO_FACTOR_RESCUE"), cv("TWO_FACTOR_RESCUE_MAIL"), cv("TWO_FACTOR_RESCUE_EMAIL_TEMPLATE"), user=form.user, ) # drop through to GET path else: return "", 404 if _security._want_json(request): return base_render_json( form, include_user=False, additional=dict(recovery_options=recovery_options) ) code_form = _build_form("two_factor_verify_code_form") return _security.render_template( cv("TWO_FACTOR_VERIFY_CODE_TEMPLATE"), two_factor_verify_code_form=code_form, two_factor_rescue_form=form, chosen_method=localize_callback( _setup_methods_xlate[form.user.tf_primary_method] ), rescue_mail=cv("TWO_FACTOR_RESCUE_MAIL"), problem=rproblem, **_ctx("tf_token_validation"), ) @anonymous_user_required @unauth_csrf() def recover_username() -> ResponseReturnValue: """View function for username recovery""" form = t.cast( UsernameRecoveryForm, _build_form_from_request("username_recovery_form") ) if form.validate_on_submit(): send_username_recovery_email(form.user) if _security._want_json(request): return base_render_json(form, include_user=False) do_flash(*get_message("USERNAME_RECOVERY_REQUEST")) return redirect(url_for_security("login")) elif request.method == "POST" and cv("RETURN_GENERIC_RESPONSES"): rinfo: dict[str, dict[str, str]] = dict(email=dict()) form_errors_munge(form, rinfo) if not form.errors: if not _security._want_json(request): do_flash(*get_message("USERNAME_RECOVERY_REQUEST")) if _security._want_json(request): return base_render_json(form, include_user=False) if rurl := _confirm_redirect(form, "email"): return rurl return _security.render_template( cv("USERNAME_RECOVERY_TEMPLATE"), username_recovery_form=form, **_ctx("recover_username"), ) def _create_blueprint(app, state, import_name): """Creates the security extension blueprint""" bp = Blueprint( cv("BLUEPRINT_NAME", app=app), import_name, url_prefix=cv("URL_PREFIX", app=app), subdomain=cv("SUBDOMAIN", app=app), template_folder="templates", static_folder=cv("STATIC_FOLDER", app), static_url_path=cv("STATIC_FOLDER_URL", app), ) if cv("LOGOUT_METHODS", app=app) is not None: bp.route( cv("LOGOUT_URL", app=app), methods=cv("LOGOUT_METHODS", app=app), endpoint="logout", )(logout) login_url = cv("LOGIN_URL", app=app) if state.passwordless: bp.route(login_url, methods=["GET", "POST"], endpoint="login")(send_login) bp.route( login_url + _slash_url_suffix(login_url, ""), endpoint="token_login", )(token_login) elif cv("US_SIGNIN_REPLACES_LOGIN", app=app): bp.route(login_url, methods=["GET", "POST"], endpoint="login")(us_signin) else: bp.route(login_url, methods=["GET", "POST"], endpoint="login")(login) if cv("FRESHNESS", app=app).total_seconds() >= 0: bp.route(cv("VERIFY_URL", app=app), methods=["GET", "POST"], endpoint="verify")( verify ) if state.unified_signin: us_signin_url = cv("US_SIGNIN_URL", app=app) us_signin_send_code_url = cv("US_SIGNIN_SEND_CODE_URL", app=app) us_setup_url = cv("US_SETUP_URL", app=app) us_verify_url = cv("US_VERIFY_URL", app=app) us_verify_send_code_url = cv("US_VERIFY_SEND_CODE_URL", app=app) us_verify_link_url = cv("US_VERIFY_LINK_URL", app=app) bp.route(us_signin_url, methods=["GET", "POST"], endpoint="us_signin")( us_signin ) bp.route( us_signin_send_code_url, methods=["POST"], endpoint="us_signin_send_code", )(us_signin_send_code) bp.route(us_setup_url, methods=["GET", "POST"], endpoint="us_setup")(us_setup) bp.route( us_setup_url + _slash_url_suffix(us_setup_url, ""), methods=["POST"], endpoint="us_setup_validate", )(us_setup_validate) # Freshness verification if cv("FRESHNESS", app=app).total_seconds() >= 0: bp.route(us_verify_url, methods=["GET", "POST"], endpoint="us_verify")( us_verify ) bp.route( us_verify_send_code_url, methods=["POST"], endpoint="us_verify_send_code", )(us_verify_send_code) bp.route(us_verify_link_url, methods=["GET"], endpoint="us_verify_link")( us_verify_link ) if state.two_factor: two_factor_setup_url = cv("TWO_FACTOR_SETUP_URL", app=app) two_factor_token_validation_url = cv("TWO_FACTOR_TOKEN_VALIDATION_URL", app=app) two_factor_rescue_url = cv("TWO_FACTOR_RESCUE_URL", app=app) bp.route( two_factor_setup_url, methods=["GET", "POST"], endpoint="two_factor_setup", )(two_factor_setup) bp.route( two_factor_setup_url + _slash_url_suffix(two_factor_setup_url, ""), methods=["POST"], endpoint="two_factor_setup_validate", )(two_factor_setup_validate) bp.route( two_factor_token_validation_url, methods=["GET", "POST"], endpoint="two_factor_token_validation", )(two_factor_token_validation) bp.route( two_factor_rescue_url, methods=["GET", "POST"], endpoint="two_factor_rescue", )(two_factor_rescue) if state.registerable: bp.route( cv("REGISTER_URL", app=app), methods=["GET", "POST"], endpoint="register" )(register) if state.recoverable: reset_url = cv("RESET_URL", app=app) bp.route(reset_url, methods=["GET", "POST"], endpoint="forgot_password")( forgot_password ) bp.route( reset_url + _slash_url_suffix(reset_url, ""), methods=["GET", "POST"], endpoint="reset_password", )(reset_password) if state.username_recovery: username_recovery_url = cv("USERNAME_RECOVERY_URL", app=app) bp.route( username_recovery_url, methods=["GET", "POST"], endpoint="recover_username", )(recover_username) if state.changeable: bp.route( cv("CHANGE_URL", app=app), methods=["GET", "POST"], endpoint="change_password", )(change_password) if state.change_email: change_email_url = cv("CHANGE_EMAIL_URL", app=app) bp.route( change_email_url, methods=["GET", "POST"], endpoint="change_email", )(change_email) bp.route( change_email_url + _slash_url_suffix(change_email_url, ""), methods=["GET"], endpoint="change_email_confirm", )(change_email_confirm) if state.change_username: bp.route( cv("CHANGE_USERNAME_URL", app=app), methods=["GET", "POST"], endpoint="change_username", )(change_username) if state.confirmable: confirm_url = cv("CONFIRM_URL", app=app) bp.route(confirm_url, methods=["GET", "POST"], endpoint="send_confirmation")( send_confirmation ) bp.route( confirm_url + _slash_url_suffix(confirm_url, ""), methods=["GET", "POST"], endpoint="confirm_email", )(confirm_email) if cv("MULTI_FACTOR_RECOVERY_CODES", app) and state.support_mfa: multi_factor_recovery_codes_url = cv("MULTI_FACTOR_RECOVERY_CODES_URL", app=app) multi_factor_recovery_url = cv("MULTI_FACTOR_RECOVERY_URL", app=app) bp.route( multi_factor_recovery_codes_url, methods=["GET", "POST"], endpoint="mf_recovery_codes", )(mf_recovery_codes) bp.route( multi_factor_recovery_url, methods=["GET", "POST"], endpoint="mf_recovery", )(mf_recovery) if state.webauthn: wan_register_url = cv("WAN_REGISTER_URL", app=app) wan_signin_url = cv("WAN_SIGNIN_URL", app=app) wan_delete_url = cv("WAN_DELETE_URL", app=app) wan_verify_url = cv("WAN_VERIFY_URL", app=app) bp.route( wan_register_url, methods=["GET", "POST"], endpoint="wan_register", )(webauthn_register) bp.route( wan_register_url + _slash_url_suffix(wan_register_url, ""), methods=["POST"], endpoint="wan_register_response", )(webauthn_register_response) bp.route(wan_signin_url, methods=["GET", "POST"], endpoint="wan_signin")( webauthn_signin ) bp.route( wan_signin_url + _slash_url_suffix(wan_signin_url, ""), methods=["POST"], endpoint="wan_signin_response", )(webauthn_signin_response) bp.route(wan_delete_url, methods=["GET", "POST"], endpoint="wan_delete")( webauthn_delete ) if cv("FRESHNESS", app=app).total_seconds() >= 0 and cv( "WAN_ALLOW_AS_VERIFY", app=app ): bp.route(wan_verify_url, methods=["GET", "POST"], endpoint="wan_verify")( webauthn_verify ) bp.route( wan_verify_url + _slash_url_suffix(wan_verify_url, ""), methods=["POST"], endpoint="wan_verify_response", )(webauthn_verify_response) if state.refresh_token: refresh_url = cv("REFRESH_TOKEN_URL", app=app) bp.route( refresh_url, methods=["POST"], endpoint="refresh_token", )(refresh) return bp pallets-eco-flask-security-72bf929/flask_security/webauthn.py000066400000000000000000001107341525721743500244720ustar00rootroot00000000000000""" flask_security.webauthn ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Flask-Security WebAuthn module :copyright: (c) 2021-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. This implements support for webauthn/FIDO2 Level 2 using the py_webauthn package. Check out: https://golb.hplar.ch/2019/08/webauthn.html for some ideas on recovery and adding additional authenticators. For testing - you can see your YubiKey (or other) resident keys in chrome! chrome://settings/securityKeys Observation: if key isn't resident than Chrome for example won't let you use it if it isn't part of allowedCredentials - throw error: referencing: https://www.w3.org/TR/webauthn-2/#sctn-privacy-considerations-client TODO: - update/add examples to support webauthn - should we universally add endpoint urls to JSON responses? - Add a way to order registered credentials so we can return an ordered list in allowCredentials. - #sctn-usecase-new-device-registration - allow more than one "first" key and have them not necessarily be cross-platform.. add form option? Research: - should we store things like user verified in 'last use'... - By insisting on 2FA if user has registered a webauthn - things get interesting if they try to log in on a different device.... How would they register a security key for a new device? They would need some OTHER 2FA? Force them to register a NEW webauthn key? """ from __future__ import annotations import json import time import typing as t from functools import partial from flask import abort, after_this_request, request, session from flask import current_app from flask_login import current_user from wtforms import BooleanField, HiddenField, RadioField, StringField, SubmitField from .forms import NextFormMixin try: import webauthn from webauthn.authentication.verify_authentication_response import ( VerifiedAuthentication, ) from webauthn.registration.verify_registration_response import VerifiedRegistration from webauthn.helpers import ( options_to_json_dict, parse_registration_credential_json, parse_authentication_credential_json, ) from webauthn.helpers.exceptions import ( InvalidAuthenticationResponse, InvalidJSONStructure, InvalidRegistrationResponse, ) from webauthn.helpers.structs import ( AuthenticatorTransport, PublicKeyCredentialDescriptor, PublicKeyCredentialType, UserVerificationRequirement, ) from webauthn.helpers import bytes_to_base64url except ImportError: # pragma: no cover pass from .decorators import anonymous_user_required, auth_required, unauth_csrf from .forms import ( Form, RequiredLocalize, _build_form_from_request, _build_form, _get_form_field_label, get_form_field_xlate, _setup_methods_xlate, IsString, ) from .proxies import _security, _datastore from .quart_compat import get_quart_status from .signals import wan_registered, wan_deleted from .tf_plugin import TfPluginBase, tf_set_validity_token_cookie from .utils import ( _, base_render_json, check_and_get_token_status, _config_value as cv, do_flash, get_message, _get_post_login_redirect, _get_post_verify_redirect, get_url, login_user, lookup_identity, _propagate_next, simple_render_json, url_for_security, _view_commit, localize_callback, allowed_auth_token, _td_format, ) if t.TYPE_CHECKING: # pragma: no cover import flask from flask.typing import ResponseValue, ResponseReturnValue from flask_security import Security, UserMixin, WebAuthnMixin if get_quart_status(): # pragma: no cover from quart import redirect else: from flask import redirect class WebAuthnRegisterForm(Form): name: StringField = StringField( get_form_field_xlate(_("Nickname")), validators=[IsString(), RequiredLocalize(message="WEBAUTHN_NAME_REQUIRED")], ) usage: RadioField = RadioField( get_form_field_xlate(_("Usage")), choices=[ ("first", get_form_field_xlate(_("Use as a first authentication factor"))), ( "secondary", get_form_field_xlate(_("Use as a secondary authentication factor")), ), ], default="secondary", validate_choice=True, ) submit: SubmitField = SubmitField( label=_get_form_field_label("submit"), id="wan_register" ) clean_name: str | None def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False assert isinstance(self.name.errors, list) assert self.name.data msg, self.clean_name = _security.webauthn_util.name_svn(self.name.data) if msg: self.name.errors.append(msg) return False if any([self.clean_name == cred.name for cred in current_user.webauthn]): msg = get_message("WEBAUTHN_NAME_INUSE", name=self.clean_name)[0] self.name.errors.append(msg) return False if not cv("WAN_ALLOW_AS_FIRST_FACTOR"): self.usage.data = "secondary" return True class WebAuthnRegisterResponseForm(Form): credential: HiddenField = HiddenField() submit: SubmitField = SubmitField(label=_get_form_field_label("submit")) # from state challenge: str name: str usage: str user_verification: bool # this is returned to caller (not part of the client form) registration_verification: VerifiedRegistration transports: list[AuthenticatorTransport] = [] extensions: str def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False # pragma: no cover assert isinstance(self.credential.errors, list) if not self.credential.data: self.credential.errors.append(get_message("API_ERROR")[0]) return False inuse = any([self.name == cred.name for cred in current_user.webauthn]) if inuse: msg = get_message("WEBAUTHN_NAME_INUSE", name=self.name)[0] self.credential.errors.append(msg) return False try: reg_cred = parse_registration_credential_json(self.credential.data) except ( ValueError, KeyError, InvalidJSONStructure, InvalidRegistrationResponse, ): self.credential.errors.append(get_message("API_ERROR")[0]) return False try: self.registration_verification = webauthn.verify_registration_response( credential=reg_cred, expected_challenge=self.challenge.encode(), expected_origin=_security.webauthn_util.origin(), expected_rp_id=request.host.split(":")[0], require_user_verification=self.user_verification, ) if _datastore.find_webauthn(credential_id=reg_cred.raw_id): msg = get_message("WEBAUTHN_CREDENTIAL_ID_INUSE")[0] self.credential.errors.append(msg) return False except KeyError: self.credential.errors.append(get_message("API_ERROR")[0]) return False except InvalidRegistrationResponse as exc: self.credential.errors.append( get_message("WEBAUTHN_NO_VERIFY", cause=str(exc))[0] ) return False self.transports = ( reg_cred.response.transports if reg_cred.response.transports else [] ) # Alas py_webauthn doesn't support extensions response_full = json.loads(self.credential.data) # TODO - verify this is JSON (created with JSON.stringify) self.extensions = response_full.get("extensions", None) return True class WebAuthnSigninForm(Form, NextFormMixin): identity: StringField = StringField( _get_form_field_label("identity"), validators=[IsString()] ) remember: BooleanField = BooleanField( _get_form_field_label("remember_me"), default=lambda: cv("DEFAULT_REMEMBER_ME", app=current_app), ) submit: SubmitField = SubmitField( label=get_form_field_xlate(_("Start")), id="wan_signin" ) user: UserMixin | None = None # set by caller - is this a second factor authentication? is_secondary: bool def __init__(self, *args: t.Any, **kwargs: t.Any): super().__init__(*args, **kwargs) def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False # pragma: no cover user = None if self.is_secondary: if "tf_user_id" in session: user = _datastore.find_user(fs_uniquifier=session["tf_user_id"]) elif cv("WAN_ALLOW_USER_HINTS"): # If we allow HINTS - provide them - but don't error # out if an unknown or disabled account - that would provide too # much 'discovery' capability of un-authenticated users. if self.identity.data: user = lookup_identity(self.identity.data) if user and user.is_active: self.user = user return True class WebAuthnSigninResponseForm(Form, NextFormMixin): """ This form is used both for signin (primary/first or secondary) and verify. """ remember: HiddenField = HiddenField() submit: SubmitField = SubmitField(label=_get_form_field_label("submit")) credential: HiddenField = HiddenField() # set by caller challenge: str user_verification: bool is_secondary: bool is_verify: bool # returned to caller authentication_verification: VerifiedAuthentication user: UserMixin | None = None cred: WebAuthnMixin | None = None # Set to True if this authentication qualifies as 'multifactor' mf_check: bool = False def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False # pragma: no cover assert isinstance(self.credential.errors, list) if not self.credential.data: self.credential.errors.append(get_message("API_ERROR")[0]) return False try: auth_cred = parse_authentication_credential_json(self.credential.data) except ( ValueError, KeyError, InvalidJSONStructure, InvalidAuthenticationResponse, ): self.credential.errors.append(get_message("API_ERROR")[0]) return False # Look up credential Id (raw_id) and user. 7.2.6/7 self.cred = _datastore.find_webauthn(credential_id=auth_cred.raw_id) if not self.cred: self.credential.errors.append( get_message("WEBAUTHN_UNKNOWN_CREDENTIAL_ID")[0] ) return False # This shouldn't be able to happen if datastore properly cascades # delete self.user = _datastore.find_user_from_webauthn(self.cred) if not self.user: # pragma: no cover self.credential.errors.append( get_message("WEBAUTHN_ORPHAN_CREDENTIAL_ID")[0] ) return False # Verify user Handle. 7.2.6 if auth_cred.response.user_handle: if ( auth_cred.response.user_handle != self.user.fs_webauthn_user_handle.encode() ): self.credential.errors.append( get_message("WEBAUTHN_MISMATCH_USER_HANDLE")[0] ) return False # Make sure the usage of credential matches configured if self.is_verify: usage = cv("WAN_ALLOW_AS_VERIFY") elif self.is_secondary: usage = "secondary" else: usage = "first" if not _is_cred_usable(self.cred, usage): self.credential.errors.append( get_message("WEBAUTHN_CREDENTIAL_WRONG_USAGE")[0] ) return False if not self.user.is_active: self.credential.errors.append(get_message("DISABLED_ACCOUNT")[0]) if self.user.is_locked(self.credential.errors): return False verify = partial( webauthn.verify_authentication_response, credential=auth_cred, expected_challenge=self.challenge.encode(), expected_origin=_security.webauthn_util.origin(), expected_rp_id=request.host.split(":")[0], credential_public_key=self.cred.public_key, credential_current_sign_count=self.cred.sign_count, ) # Start by verifying requiring user_verification - if that succeeds, then # this authn could be used for both primary and secondary. # If it fails, then try to verify with user_verification == False - unless # as part of signin the app required user_verification (as stored in the state) try: self.authentication_verification = verify(require_user_verification=True) self.mf_check = True except InvalidAuthenticationResponse: try: self.authentication_verification = verify( require_user_verification=self.user_verification ) except InvalidAuthenticationResponse as exc: self.credential.errors.append( get_message("WEBAUTHN_NO_VERIFY", cause=str(exc))[0] ) self.user.track_failed_authn("passkey") return False return True class WebAuthnDeleteForm(Form): # Change id of name since this shows up on register form that ALSO has a name # element. name: StringField = StringField( get_form_field_xlate(_("Nickname")), validators=[IsString(), RequiredLocalize(message="WEBAUTHN_NAME_REQUIRED")], id="delete-name", ) submit: SubmitField = SubmitField(label=_get_form_field_label("delete")) clean_name: str | None def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False assert isinstance(self.name.errors, list) assert self.name.data msg, self.clean_name = _security.webauthn_util.name_svn(self.name.data) if msg: self.name.errors.append(msg) return False if not any([self.clean_name == cred.name for cred in current_user.webauthn]): self.name.errors.append( get_message("WEBAUTHN_NAME_NOT_FOUND", name=self.name.data)[0] ) return False return True class WebAuthnVerifyForm(Form): submit: SubmitField = SubmitField( label=_get_form_field_label("submit"), id="wan_verify" ) user: UserMixin def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): return False # pragma: no cover # We are always authenticated - so return possible credentials. self.user = current_user return True @auth_required( lambda: cv("API_ENABLED_METHODS"), within=lambda: cv("FRESHNESS"), grace=lambda: cv("FRESHNESS_GRACE_PERIOD"), ) def webauthn_register() -> ResponseReturnValue: """Start Registration for an existing authenticated user Note that it requires a POST to start the registration and must send 'name' in. We check here that user hasn't already registered an authenticator with that name. Also - this requires that the user already be logged in - so we can provide info as part of the GET that could otherwise be considered leaking user info. """ payload: dict[str, t.Any] form: WebAuthnRegisterForm = t.cast( WebAuthnRegisterForm, _build_form_from_request("wan_register_form") ) if form.validate_on_submit(): challenge = _security.webauthn_util.generate_challenge( cv("WAN_CHALLENGE_BYTES") ) if not current_user.fs_webauthn_user_handle: # set a user handle. This allows an easy migration when adding this # column (and not requiring as part of schema change to update all existing # records). New users will have this set as part of user creation. after_this_request(_view_commit) _datastore.set_webauthn_user_handle(current_user) ro = dict( challenge=challenge.encode(), rp_name=cv("WAN_RP_NAME"), rp_id=request.host.split(":")[0], user_id=current_user.fs_webauthn_user_handle.encode(), user_name=current_user.calc_username(), timeout=cv("WAN_REGISTER_TIMEOUT"), exclude_credentials=_create_credential_list( current_user, ["first", "secondary"] ), ) ro = _security.webauthn_util.registration_options( current_user, form.usage.data, ro ) credential_options = webauthn.generate_registration_options(**ro) co_json = options_to_json_dict(credential_options) co_json["extensions"] = {"credProps": True} # If we ask for UserVerification then we need to check that in the response. uv = False if credential_options.authenticator_selection: uv = ( credential_options.authenticator_selection.user_verification == UserVerificationRequirement.REQUIRED ) state = { "challenge": challenge, "name": form.clean_name, "usage": form.usage.data, "user_verification": uv, } state_token = _security.wan_serializer.dumps(state) if _security._want_json(request): payload = { "credential_options": co_json, "wan_state": state_token, } return base_render_json(form, include_user=False, additional=payload) return _security.render_template( cv("WAN_REGISTER_TEMPLATE"), wan_register_form=form, wan_register_response_form=_build_form("wan_register_response_form"), wan_state=state_token, credential_options=json.dumps(co_json), **_security._run_ctx_processor("wan_register"), ) current_creds = [] cred: WebAuthnMixin for cred in current_user.webauthn: cl = { "name": cred.name, "credential_id": bytes_to_base64url(cred.credential_id), "transports": cred.transports, "lastuse": cred.lastuse_datetime.isoformat(), "usage": cred.usage, "backup_state": ( cred.backup_state if hasattr(cred, "backup_state") else False ), "device_type": ( cred.device_type if hasattr(cred, "device_type") else "Unknown" ), } # TODO: i18n discoverable = "Unknown" if cred.extensions: extensions = json.loads(cred.extensions) if "credProps" in extensions: discoverable = extensions["credProps"].get("rk", "Unknown") cl["discoverable"] = discoverable current_creds.append(cl) payload = {"registered_credentials": current_creds} if _security._want_json(request): return base_render_json(form, additional=payload) return _security.render_template( cv("WAN_REGISTER_TEMPLATE"), wan_register_form=form, wan_delete_form=_build_form("wan_delete_form"), registered_credentials=current_creds, **_security._run_ctx_processor("wan_register"), ) @auth_required(lambda: cv("API_ENABLED_METHODS")) def webauthn_register_response(token: str) -> ResponseReturnValue: """Response from browser.""" form: WebAuthnRegisterResponseForm = t.cast( WebAuthnRegisterResponseForm, _build_form_from_request("wan_register_response_form"), ) expired, invalid, state = check_and_get_token_status( token, "wan", cv("WAN_REGISTER_WITHIN") ) if invalid: m, c = get_message("API_ERROR") if expired: m, c = get_message( "WEBAUTHN_EXPIRED", within=_td_format(cv("WAN_REGISTER_WITHIN")) ) if invalid or expired: if _security._want_json(request): form.form_errors.append(m) return base_render_json(form, include_user=False) do_flash(m, c) return redirect(url_for_security("wan_register")) form.challenge = state["challenge"] form.name = state["name"] form.usage = state["usage"] form.user_verification = state["user_verification"] if form.validate_on_submit(): # store away successful registration after_this_request(_view_commit) _datastore.create_webauthn( current_user._get_current_object(), # Not needed with Werkzeug >2.0.0 name=state["name"], credential_id=form.registration_verification.credential_id, public_key=form.registration_verification.credential_public_key, sign_count=form.registration_verification.sign_count, backup_state=getattr( form.registration_verification, "credential_backed_up", False ), device_type=getattr( form.registration_verification, "credential_device_type", "single_device", ), transports=list(form.transports), extensions=form.extensions, usage=form.usage, ) wan_registered.send( current_app._get_current_object(), # type: ignore _async_wrapper=current_app.ensure_sync, # type: ignore[arg-type] user=current_user, name=state["name"], ) if _security._want_json(request): return base_render_json(form) msg, c = get_message("WEBAUTHN_REGISTER_SUCCESSFUL", name=state["name"]) do_flash(msg, c) return redirect(get_url(cv("WAN_POST_REGISTER_VIEW"))) if _security._want_json(request): return base_render_json(form) if form.errors: for v in form.errors.values(): do_flash(v[0], "error") return redirect(url_for_security("wan_register")) def _signin_common(user: UserMixin | None, usage: list[str]) -> tuple[t.Any, str]: """ Common code between signin and verify - once form has been verified. """ challenge = _security.webauthn_util.generate_challenge(cv("WAN_CHALLENGE_BYTES")) # Populate allowedCredentials if identity passed and allowed allow_credentials = None if user: allow_credentials = _create_credential_list(user, usage) ao = dict( rp_id=request.host.split(":")[0], challenge=challenge.encode(), timeout=cv("WAN_SIGNIN_TIMEOUT"), allow_credentials=allow_credentials, ) ao = _security.webauthn_util.authentication_options(user, usage, ao) options = webauthn.generate_authentication_options(**ao) # If we ask for UserVerification then we need to check that in the response. uv = False if options.user_verification == UserVerificationRequirement.REQUIRED: uv = True state = { "challenge": challenge, "user_verification": uv, } o_json = options_to_json_dict(options) state_token = t.cast( # type: ignore[redundant-cast] str, _security.wan_serializer.dumps(state) ) return o_json, state_token @anonymous_user_required @unauth_csrf() def webauthn_signin() -> ResponseReturnValue: # This view can be called either as a 'first' authentication or as part of # 2FA. is_secondary = all(k in session for k in ["tf_user_id", "tf_state"]) and session[ "tf_state" ] in ["ready"] if is_secondary or cv("WAN_ALLOW_AS_FIRST_FACTOR"): pass else: abort(404) form = t.cast(WebAuthnSigninForm, _build_form_from_request("wan_signin_form")) form.is_secondary = is_secondary if form.validate_on_submit(): o_json, state_token = _signin_common( form.user, ["secondary"] if is_secondary else ["first"] ) if _security._want_json(request): payload = { "credential_options": o_json, "wan_state": state_token, "remember": form.remember.data, "is_secondary": is_secondary, } return base_render_json(form, include_user=False, additional=payload) # Copy the user's remember field into the next form - since that is # auto-submitted. return _security.render_template( cv("WAN_SIGNIN_TEMPLATE"), wan_signin_form=form, wan_signin_response_form=_build_form( "wan_signin_response_form", remember=form.remember.data, next=form.next.data, ), wan_state=state_token, credential_options=json.dumps(o_json), is_secondary=is_secondary, **_security._run_ctx_processor("wan_signin"), ) if _security._want_json(request): return base_render_json(form, additional={"is_secondary": is_secondary}) return _security.render_template( cv("WAN_SIGNIN_TEMPLATE"), wan_signin_form=form, wan_signin_response_form=_build_form("wan_signin_response_form"), is_secondary=is_secondary, **_security._run_ctx_processor("wan_signin"), ) @unauth_csrf() def webauthn_signin_response(token: str) -> ResponseReturnValue: is_secondary = all(k in session for k in ["tf_user_id", "tf_state"]) and session[ "tf_state" ] in ["ready"] form = t.cast( WebAuthnSigninResponseForm, _build_form_from_request("wan_signin_response_form") ) expired, invalid, state = check_and_get_token_status( token, "wan", cv("WAN_SIGNIN_WITHIN") ) if invalid: m, c = get_message("API_ERROR") if expired: m, c = get_message( "WEBAUTHN_EXPIRED", within=_td_format(cv("WAN_SIGNIN_WITHIN")) ) if invalid or expired: if _security._want_json(request): form.form_errors.append(m) return base_render_json(form, include_user=False) do_flash(m, c) return redirect(url_for_security("wan_signin")) form.challenge = state["challenge"] form.user_verification = state["user_verification"] form.is_secondary = is_secondary form.is_verify = False if form.validate_on_submit(): # update last use and sign count after_this_request(_view_commit) assert form.cred assert form.user form.cred.lastuse_datetime = _security.datetime_factory() form.cred.sign_count = form.authentication_verification.new_sign_count form.cred.backup_state = getattr( form.authentication_verification, "credential_backed_up", False ) form.cred.device_type = getattr( form.authentication_verification, "credential_device_type", "single_device" ) _datastore.put(form.cred) json_payload: dict[str, t.Any] = {} if is_secondary: tf_token = _security.two_factor_plugins.tf_complete(form.user, True) if tf_token: after_this_request( partial(tf_set_validity_token_cookie, token=tf_token) ) else: # Need Two-factor?: # - Is it required? # - Did this credential provide 2-factor and # is WAN_ALLOW_AS_MULTI_FACTOR set # - Is another 2FA setup? remember_me = bool(form.remember.data) if form.mf_check and cv("WAN_ALLOW_AS_MULTI_FACTOR"): pass else: response = _security.two_factor_plugins.tf_enter( form.user, remember_me, "webauthn", next_loc=_propagate_next(request.url, form), ) if response: return response # login user login_user(form.user, remember=remember_me, authn_via=["webauthn"]) goto_url = _get_post_login_redirect() if _security._want_json(request): # Tell caller where we would go if forms based - they can use it or # not. json_payload["post_login_url"] = goto_url json_payload["tf_required"] = False return base_render_json( form, include_auth_token=allowed_auth_token(form.user), additional=json_payload, ) return redirect(goto_url) # Here on validate error if _security._want_json(request): return base_render_json(form) # Since the response is auto submitted - we go back to # signin form - for now use flash. if form.errors: for v in form.errors.values(): do_flash(v[0], "error") return redirect(url_for_security("wan_signin")) @auth_required( lambda: cv("API_ENABLED_METHODS"), within=lambda: cv("FRESHNESS"), grace=lambda: cv("FRESHNESS_GRACE_PERIOD"), ) def webauthn_delete() -> ResponseReturnValue: """Deletes an existing registered credential.""" form = t.cast(WebAuthnDeleteForm, _build_form_from_request("wan_delete_form")) if form.validate_on_submit(): # validate made sure form.name.data exists. cred = [c for c in current_user.webauthn if c.name == form.clean_name][0] after_this_request(_view_commit) wan_deleted.send( current_app._get_current_object(), # type: ignore _async_wrapper=current_app.ensure_sync, # type: ignore[arg-type] user=current_user, name=cred.name, ) _datastore.delete_webauthn(cred) if _security._want_json(request): return base_render_json(form) msg, c = get_message("WEBAUTHN_CREDENTIAL_DELETED", name=form.clean_name) do_flash(msg, c) if _security._want_json(request): return base_render_json(form) if form.name.errors: do_flash(form.name.errors[0], "error") return redirect(url_for_security("wan_register")) @auth_required(lambda: cv("API_ENABLED_METHODS")) def webauthn_verify() -> ResponseReturnValue: """ Re-authenticate to reset freshness time. This is likely the result of a reauthn_handler redirect, which will have filled in ?next=xxx - which we want to carefully not lose as we go through these steps. """ form = t.cast(WebAuthnVerifyForm, _build_form_from_request("wan_verify_form")) if form.validate_on_submit(): o_json, state_token = _signin_common(form.user, cv("WAN_ALLOW_AS_VERIFY")) if _security._want_json(request): payload = {"credential_options": o_json, "wan_state": state_token} return base_render_json(form, include_user=False, additional=payload) return _security.render_template( cv("WAN_VERIFY_TEMPLATE"), wan_verify_form=form, wan_signin_response_form=_build_form("wan_signin_response_form"), wan_state=state_token, credential_options=json.dumps(o_json), **_security._run_ctx_processor("wan_verify"), ) if _security._want_json(request): return base_render_json(form) return _security.render_template( cv("WAN_VERIFY_TEMPLATE"), wan_verify_form=form, wan_signin_response_form=_build_form("wan_signin_response_form"), skip_login_menu=True, **_security._run_ctx_processor("wan_verify"), ) @auth_required(lambda: cv("API_ENABLED_METHODS")) def webauthn_verify_response(token: str) -> ResponseReturnValue: form = t.cast( WebAuthnSigninResponseForm, _build_form_from_request("wan_signin_response_form") ) expired, invalid, state = check_and_get_token_status( token, "wan", cv("WAN_SIGNIN_WITHIN") ) if invalid: m, c = get_message("API_ERROR") if expired: m, c = get_message( "WEBAUTHN_EXPIRED", within=_td_format(cv("WAN_SIGNIN_WITHIN")) ) if invalid or expired: if _security._want_json(request): form.form_errors.append(m) return base_render_json(form, include_user=False) do_flash(m, c) return redirect(url_for_security("wan_verify")) form.challenge = state["challenge"] form.user_verification = state["user_verification"] form.is_secondary = False form.is_verify = True if form.validate_on_submit(): assert form.cred assert form.user # The assertion was cryptographically valid for `form.user`. For the # reauthentication / verify flow, that user must also be the user # currently logged in to this session - otherwise an attacker who # owns any registered credential could satisfy a victim session's # freshness gate by submitting their own credential's assertion. if form.user.email != current_user.email: m, c = get_message("WEBAUTHN_MISMATCH_USER_HANDLE") if _security._want_json(request): form.form_errors.append(m) return base_render_json(form, include_user=False) do_flash(m, c) return redirect(url_for_security("wan_verify")) # update last use and sign count after_this_request(_view_commit) form.cred.lastuse_datetime = _security.datetime_factory() form.cred.sign_count = form.authentication_verification.new_sign_count _datastore.put(form.cred) # verified - so set freshness time. session["fs_paa"] = time.time() if _security._want_json(request): return base_render_json( form, include_auth_token=allowed_auth_token(form.user) ) do_flash(*get_message("REAUTHENTICATION_SUCCESSFUL")) return redirect(_get_post_verify_redirect()) # Here on validate error (only POST is allowed on this endpoint) if _security._want_json(request): return base_render_json(form) # Since the response is auto submitted - we go back to # verify form - for now use flash. if form.credential.errors: do_flash(form.credential.errors[0], "error") return redirect(url_for_security("wan_verify")) def _is_cred_usable(cred: WebAuthnMixin, usage: str | list[str]) -> bool: # Return True is cred can be used for the requested usage/verify if not isinstance(usage, list): usage = [usage] assert "verify" not in usage return cred.usage in usage def has_webauthn(user: UserMixin, usage: str | list[str]) -> bool: # Return True if ``user`` has one or more keys with requested usage. # Usage: either "first" or "secondary" if not isinstance(usage, list): usage = [usage] wan_keys = getattr(user, "webauthn", []) for cred in wan_keys: if _is_cred_usable(cred, usage): return True return False def _create_credential_list( user: UserMixin, usage: list[str] ) -> list[PublicKeyCredentialDescriptor]: # Return a list of registered credentials - filtered by whether they apply to our # authentication state (first or secondary) cl = [] for cred in user.webauthn: if not _is_cred_usable(cred, usage): continue descriptor = PublicKeyCredentialDescriptor( type=PublicKeyCredentialType.PUBLIC_KEY, id=cred.credential_id ) if cred.transports: tlist = cred.transports transports = [AuthenticatorTransport(transport) for transport in tlist] descriptor.transports = transports # TODO order is important - figure out a way to add 'weight' cl.append(descriptor) return cl class WebAuthnTfPlugin(TfPluginBase): def __init__(self, app: flask.Flask): super().__init__(app) def create_blueprint( self, app: flask.Flask, bp: flask.Blueprint, state: Security ) -> None: """Our endpoints are already registered since webauthn can be both a 'first' or 'secondary' authentication mechanism. """ pass def get_setup_methods(self, user: UserMixin) -> list[tuple[str, str]]: if has_webauthn(user, "secondary"): return [("webauthn", localize_callback(_setup_methods_xlate["webauthn"]))] return [] def tf_login( self, user: UserMixin, json_payload: dict[str, t.Any], next_loc: str | None ) -> ResponseValue: session["tf_state"] = "ready" if not _security._want_json(request): values = dict(next=next_loc) if next_loc else dict() return redirect(url_for_security("wan_signin", **values)) # JSON response json_payload["tf_signin_url"] = url_for_security("wan_signin") json_payload["tf_state"] = "ready" json_payload["tf_method"] = "webauthn" return simple_render_json(additional=json_payload) pallets-eco-flask-security-72bf929/flask_security/webauthn_util.py000066400000000000000000000164521525721743500255310ustar00rootroot00000000000000""" flask_security.webauthn_util ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Utility class providing methods controlling various aspects of webauthn. :copyright: (c) 2020-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from __future__ import annotations import secrets import typing as t from flask import current_app, request from .utils import _config_value as cv, input_svn, get_message try: # noinspection PyUnresolvedReferences from webauthn.helpers.structs import ( AuthenticatorAttachment, AuthenticatorSelectionCriteria, ResidentKeyRequirement, UserVerificationRequirement, ) except ImportError: # pragma: no cover pass if t.TYPE_CHECKING: # pragma: no cover import flask from flask_security import UserMixin class WebauthnUtil: """ Utility class allowing an application to fine-tune various Relying Party attributes. To provide your own implementation, pass in the class as ``webauthn_util_cls`` at init time. Your class will be instantiated once as part of app initialization. .. versionadded:: 5.0.0 """ def __init__(self, app: flask.Flask): """Instantiate class. :param app: The Flask application being initialized. """ pass def generate_challenge(self, nbytes: int | None = None) -> str: # Mostly override this for testing, so we can have a 'constant' challenge. return secrets.token_urlsafe(nbytes) def origin(self) -> str: # Return the RP origin - normally this is just the URL of the application. return request.host_url.rstrip("/") def registration_options( self, user: UserMixin, usage: str, existing_options: dict[str, t.Any] ) -> dict[str, t.Any]: """ :param user: User object - could be used to configure on a per-user basis. :param usage: Either "first" or "secondary" (webauthn is being used as a second factor for authentication) :param existing_options: Currently filled in registration options. Return a dict that will be sent in to py-webauthn generate_registration_options """ existing_options["authenticator_selection"] = self.authenticator_selection( user, usage ) return existing_options def authenticator_selection( self, user: UserMixin, usage: str ) -> AuthenticatorSelectionCriteria: """ :param user: User object - could be used to configure on a per-user basis. :param usage: Either "first" or "secondary" (webauthn is being used as a second factor for authentication Part of the registration ceremony is providing information about what kind of authenticators the app is interested in. See: https://www.w3.org/TR/2021/REC-webauthn-2-20210408/#dictionary-authenticatorSelection The main options are: - whether you want a ResidentKey (discoverable) - Attachment - platform or cross-platform - Does the key have to provide user-verification :note:: If the key isn't resident then it isn't discoverable which means that the user won't be able to use that key unless they identify themselves (use the key as a second factor OR type in their identity). If they are forced to type in their identity PRIOR to being authenticated, then there is the possibility that the app will leak username information. """ # noqa: E501 select_criteria = AuthenticatorSelectionCriteria() # TODO: look at #sctn-usecase-new-device-registration to see a reason # to allow multiple keys as "first" - only one would need to be cross-platform if usage == "first": select_criteria.authenticator_attachment = ( AuthenticatorAttachment.CROSS_PLATFORM ) select_criteria.user_verification = UserVerificationRequirement.PREFERRED else: # For second factor minimize user-interaction by not asking for UV select_criteria.user_verification = UserVerificationRequirement.DISCOURAGED if not current_app.config.get("SECURITY_WAN_ALLOW_USER_HINTS"): select_criteria.resident_key = ResidentKeyRequirement.REQUIRED else: select_criteria.resident_key = ResidentKeyRequirement.PREFERRED return select_criteria def authentication_options( self, user: UserMixin | None, usage: list[str], existing_options: dict[str, t.Any], ) -> dict[str, t.Any]: """ :param user: User object - could be used to configure on a per-user basis. However, this can be null. :param usage: Either "first" or "secondary" (webauthn is being used as a second factor for authentication) :param existing_options: Currently filled in authentication options. Return a dict that will be sent in to py-webauthn generate_authentication_options """ existing_options["user_verification"] = self.user_verification(user, usage) return existing_options def user_verification( self, user: UserMixin | None, usage: list[str] ) -> UserVerificationRequirement: """ As part of signin - do we want/need user verification. This is called from /wan-signin and /wan-verify :param user: User object - could be used to configure on a per-user basis. Note that this may not be set on initial wan-signin. :param usage: List of "first", "secondary" (webauthn is being used as a second factor for authentication). Note that in the ``verify``/``reauthentication`` case this list is derived from :py:data:`SECURITY_WAN_ALLOW_AS_VERIFY` """ if "secondary" in usage: return UserVerificationRequirement.DISCOURAGED if current_app.config.get("SECURITY_WAN_ALLOW_AS_MULTI_FACTOR"): return UserVerificationRequirement.PREFERRED return UserVerificationRequirement.PREFERRED def name_svn(self, value: str) -> tuple[str | None, str | None]: """ Sanitize, validate, and normalize form input. Return value is a tuple (msg, clean_value). msg will be None if properly validated. The config variable :py:data:`SECURITY_WAN_NAME_ALLOWED_CHARS` specifies which unicode character classes are allowed (if None then all are allowed). The config variable :py:data:`SECURITY_INPUT_NORMALIZE_FORM` specifies the input to unicode normalization (if None then no normalization is performed). For webauthn names, allow letters, numbers, and some punctuation. """ reason, clean_input = input_svn( value, cv("WAN_NAME_ALLOWED_CHARS"), cv("INPUT_NORMALIZE_FORM") ) if clean_input is None: msg = ( "WEBAUTHN_NAME_DISALLOWED_CHARACTERS" if reason == "unallowed" else "INVALID_INPUT" ) return get_message(msg)[0], None if len(clean_input) > cv("WAN_NAME_MAX_LENGTH"): msg = "INVALID_INPUT_LENGTH" return ( get_message(msg, length=cv("WAN_NAME_MAX_LENGTH"))[0], None, ) return None, clean_input pallets-eco-flask-security-72bf929/mypy.ini000066400000000000000000000013541525721743500207500ustar00rootroot00000000000000[mypy] pretty = True show_error_codes = True # this sucks - but most of our packages don't yet have types. disable_error_code = import-untyped no_implicit_optional = True disallow_incomplete_defs = True strict_equality = True warn_redundant_casts = True # Can't figure out why it sometimes says its ignored - but PyCharm wants it warn_unused_ignores = False warn_no_return = True warn_unreachable = True # This false-positives with non-annotated methods (like imported packages) warn_return_any = False warn_unused_configs = True [mypy-flask_security.cli] # Due to click 8.1.4 ignore_errors = True [mypy-quart.*] ignore_missing_imports = True [mypy-flask_mailman.*] ignore_missing_imports = True [mypy-twilio.*] ignore_missing_imports = True pallets-eco-flask-security-72bf929/pyproject-too.toml000066400000000000000000000111661525721743500227660ustar00rootroot00000000000000[project] name = "Flask-Security-Too" description = "Quickly add security features to your Flask application." readme.content-type = "text/x-rst" readme.file = "README.rst" keywords = ["flask security"] license = { file = "LICENSE.txt" } maintainers = [{ name = "Chris Wagner", email = "jwag.wagner+github@gmail.com"}] authors = [{ name = "Matt Wright"}, { name = "Chris Wagner", email = "jwag.wagner+github@gmail.com"}] requires-python = ">=3.11" urls.Documentation = "https://flask-security.readthedocs.io" urls.Homepage = "https://github.com/pallets-eco/flask-security" urls.Source = "https://github.com/pallets-eco/flask-security" urls.Tracker = "https://github.com/pallets-eco/flask-security/issues" urls.Releases = "https://pypi.org/project/Flask-Security/" classifiers=[ "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Development Status :: 5 - Production/Stable", ] dynamic = [ "version", ] dependencies = [ # flask dependencies include werkzeug, jinja2, itsdangerous, click, blinker "Flask>=3.1.1", "Flask-Login>=0.6.3", "Flask-Principal>=0.4.0", "Flask-WTF>=1.1.2", "email-validator>=2.3.0", "markupsafe>=2.1.0", "libpass>=1.9.3", "wtforms>=3.2.1", # for form-level errors ] [project.optional-dependencies] babel = ["babel>=2.17.0", "flask_babel>=4.0.0", "humanize>=4.12.0"] fsqla = ["flask_sqlalchemy>=3.1.1", "sqlalchemy>=2.0.41"] common = ["argon2_cffi>=25.1.0", "bcrypt>=4.3.0", "flask_mail>=0.10.0", "nh3>=0.2.15"] mfa = ["cryptography>=46.0.7", "qrcode>=8.2", "phonenumberslite>=8.13.11", "webauthn>=2.8.0"] [dependency-groups] tests = [ "Flask-Babel", "Babel", "Flask-Mail", "Flask-SQLAlchemy", "Flask-SQLAlchemy-Lite", "argon2-cffi", "authlib", "bcrypt", "cbor2", "coverage", "cryptography", "djlint", "freezegun", "humanize", "mongoengine", "mongomock", "msgcheck", "nh3", "peewee", "phonenumberslite", "pydocstyle", "pytest-cache", "pytest-cov", "pytest", "qrcode", # authlib requires requests "requests", "sqlalchemy<2.1", "sqlalchemy-utils", "webauthn", "zxcvbn", ] dev = [ "flit", "check-wheel-contents", "psycopg2-binary", "pymysql", "pre-commit", "tox", # for dev - might not install Flask-Security - list those dependencies here "flask", "flask-wtf", "flask-login", "flask-principal", "markupsafe", "libpass", "email_validator", ] low = [ # Lowest supported versions "Flask==3.1.1", "Flask-SQLAlchemy==3.1.1", "Flask-SQLAlchemy-Lite==0.1.0", "Flask-Babel==4.0.0", "Flask-Mail==0.10.0", "Flask-Login==0.6.3", "Flask-WTF==1.1.2", "peewee==3.17.9", "argon2_cffi==21.3.0", "authlib==1.6.0", "babel==2.16.0", "bcrypt==4.3.0", "nh3==0.2.15", "freezegun", "humanize", "jinja2==3.1.2", "itsdangerous==2.2.0", "markupsafe==2.1.2", "mongoengine==0.29.1", "mongomock==4.3.0", "phonenumberslite==8.13.11", "qrcode==8.2.0", # authlib requires requests "requests", "sqlalchemy==2.0.41", "sqlalchemy-utils==0.42.0", "webauthn==2.8.0", "werkzeug==3.1.3", "zxcvbn==4.4.28" ] docs = [ "Pallets-Sphinx-Themes", "Sphinx", "sphinx-issues", "packaging", "Flask-SQLAlchemy", "sqlalchemy", "sqlalchemy-utils", ] typing = [ "basedpyright", "types-requests", "mypy", ] [build-system] requires = ["flit_core >=3.8,<4"] build-backend = "flit_core.buildapi" [tool.flit.module] name = "flask_security" [tool.flit.sdist] include = [ "AUTHORS", "CHANGES.rst", "CONTRIBUTING.rst", "LICENSE.txt", ".djlintrc", ".git-blame-ignore-revs", ".gitignore", ".pre-commit-config.yaml", ".readthedocs.yml", "pyproject.toml", "babel.ini", "codecov.yml", "mypy.ini", "pytest.ini", "tox.ini", "docs/", "examples/", "tests/", ] exclude = ["docs/_build/"] [tool.djlint] ignore="H005,H006" # lang, img height/width [tool.basedpyright] include=["flask_security", "tests/view_scaffold.py"] analyzeUnannotatedFunctions = false reportMissingImports = false reportAny = false reportPrivateUsage = false failOnWarnings = false pallets-eco-flask-security-72bf929/pyproject.toml000066400000000000000000000111661525721743500221670ustar00rootroot00000000000000[project] name = "Flask-Security" description = "Quickly add security features to your Flask application." readme.content-type = "text/x-rst" readme.file = "README.rst" keywords = ["flask security"] license = { file = "LICENSE.txt" } maintainers = [{ name = "Chris Wagner", email = "jwag.wagner+github@gmail.com"}] authors = [{ name = "Matt Wright"}, { name = "Chris Wagner", email = "jwag.wagner+github@gmail.com"}] requires-python = ">=3.11" urls.Documentation = "https://flask-security.readthedocs.io" urls.Homepage = "https://github.com/pallets-eco/flask-security" urls.Source = "https://github.com/pallets-eco/flask-security" urls.Tracker = "https://github.com/pallets-eco/flask-security/issues" urls.Releases = "https://pypi.org/project/Flask-Security/" classifiers=[ "Environment :: Web Environment", "Framework :: Flask", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Development Status :: 5 - Production/Stable", ] dynamic = [ "version", ] dependencies = [ # flask dependencies include werkzeug, jinja2, itsdangerous, click, blinker "Flask>=3.1.1", "Flask-Login>=0.6.3", "Flask-Principal>=0.4.0", "Flask-WTF>=1.1.2", "email-validator>=2.3.0", "markupsafe>=2.1.0", "libpass>=1.9.3", "wtforms>=3.2.1", # for form-level errors ] [project.optional-dependencies] babel = ["babel>=2.17.0", "flask_babel>=4.0.0", "humanize>=4.12.0"] fsqla = ["flask_sqlalchemy>=3.1.1", "sqlalchemy>=2.0.41"] common = ["argon2_cffi>=25.1.0", "bcrypt>=4.3.0", "flask_mail>=0.10.0", "nh3>=0.2.15"] mfa = ["cryptography>=46.0.7", "qrcode>=8.2", "phonenumberslite>=8.13.11", "webauthn>=2.8.0"] [dependency-groups] tests = [ "Flask-Babel", "Babel", "Flask-Mail", "Flask-SQLAlchemy", "Flask-SQLAlchemy-Lite", "argon2-cffi", "authlib", "bcrypt", "cbor2", "coverage", "cryptography", "djlint", "freezegun", "humanize", "mongoengine", "mongomock", "msgcheck", "nh3", "peewee", "phonenumberslite", "pydocstyle", "pytest-cache", "pytest-cov", "pytest", "qrcode", # authlib requires requests "requests", "sqlalchemy<2.1", "sqlalchemy-utils", "webauthn", "zxcvbn", ] dev = [ "flit", "check-wheel-contents", "psycopg2-binary", "pymysql", "pre-commit", "tox", # for dev - might not install Flask-Security - list those dependencies here "flask", "flask-wtf", "flask-login", "flask-principal", "markupsafe", "libpass", "email_validator", ] low = [ # Lowest supported versions "Flask==3.1.1", "Flask-SQLAlchemy==3.1.1", "Flask-SQLAlchemy-Lite==0.1.0", "Flask-Babel==4.0.0", "Flask-Mail==0.10.0", "Flask-Login==0.6.3", "Flask-WTF==1.1.2", "peewee==3.17.9", "argon2_cffi==21.3.0", "authlib==1.6.0", "babel==2.16.0", "bcrypt==4.3.0", "nh3==0.2.15", "freezegun", "humanize", "jinja2==3.1.2", "itsdangerous==2.2.0", "markupsafe==2.1.2", "mongoengine==0.29.1", "mongomock==4.3.0", "phonenumberslite==8.13.11", "qrcode==8.2.0", # authlib requires requests "requests", "sqlalchemy==2.0.41", "sqlalchemy-utils==0.42.0", "webauthn==2.8.0", "werkzeug==3.1.3", "zxcvbn==4.4.28" ] docs = [ "Pallets-Sphinx-Themes", "Sphinx", "sphinx-issues", "packaging", "Flask-SQLAlchemy", "sqlalchemy", "sqlalchemy-utils", ] typing = [ "basedpyright", "types-requests", "mypy", ] [build-system] requires = ["flit_core >=3.8,<4"] build-backend = "flit_core.buildapi" [tool.flit.module] name = "flask_security" [tool.flit.sdist] include = [ "AUTHORS", "CHANGES.rst", "CONTRIBUTING.rst", "LICENSE.txt", ".djlintrc", ".git-blame-ignore-revs", ".gitignore", ".pre-commit-config.yaml", ".readthedocs.yml", "pyproject-too.toml", "babel.ini", "codecov.yml", "mypy.ini", "pytest.ini", "tox.ini", "docs/", "examples/", "tests/", ] exclude = ["docs/_build/"] [tool.djlint] ignore="H005,H006" # lang, img height/width [tool.basedpyright] include=["flask_security", "tests/view_scaffold.py"] analyzeUnannotatedFunctions = false reportMissingImports = false reportAny = false reportPrivateUsage = false failOnWarnings = false pallets-eco-flask-security-72bf929/pytest.ini000066400000000000000000000016771525721743500213120ustar00rootroot00000000000000[pytest] addopts = -rs --cache-clear --strict-markers markers = settings app_settings babel changeable confirmable registerable two_factor recoverable oauth passwordless trackable unified_signin webauthn flask_async csrf change_email change_username username_recovery refresh_token filterwarnings = error ignore:.*Passing SelectField choices as tuples.*:DeprecationWarning:wtforms:0 ignore::DeprecationWarning:mongoengine: ignore::DeprecationWarning:flask_login:0 ignore:.*ImmutableDict.*:DeprecationWarning:flask:0 # next for py 3.12 ignore::DeprecationWarning:dateutil:0 ignore:.*passwordless feature.*:DeprecationWarning:flask_security:0 ignore:.*'sms' was enabled in SECURITY_US_ENABLED_METHODS;.*:UserWarning:flask_security:0 ignore:.*The SECURITY_USE_REGISTER_V2 configuration option is deprecated.*:DeprecationWarning:flask_security:0 pallets-eco-flask-security-72bf929/tests/000077500000000000000000000000001525721743500204105ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/tests/__init__.py000066400000000000000000000000001525721743500225070ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/tests/conftest.py000066400000000000000000001145371525721743500226220ustar00rootroot00000000000000""" conftest ~~~~~~~~ Test fixtures and what not :copyright: (c) 2017 by CERN. :copyright: (c) 2019-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from __future__ import annotations from collections.abc import Mapping import sqlite3 import gc import os import tempfile import time import typing as t import sys from urllib.parse import urlsplit from passlib.ifc import PasswordHash from passlib.registry import register_crypt_handler import pytest from flask import Flask, Response, jsonify, render_template from flask import request as flask_request from flask_mail import Mail from flask_wtf import CSRFProtect try: from sqlalchemy.orm import Mapped except ImportError: pass from flask_security import ( FSQLALiteUserDatastore, MongoEngineUserDatastore, PeeweeUserDatastore, RoleMixin, Security, SQLAlchemySessionUserDatastore, SQLAlchemyUserDatastore, UserMixin, WebAuthnMixin, auth_required, auth_token_required, http_auth_required, get_request_attr, roles_accepted, roles_required, permissions_accepted, permissions_required, uia_email_mapper, RefreshTrackerMixin, ) from flask_security.utils import localize_callback from tests.test_utils import convert_bool_option, populate_data # enable testing both register form options v2_param = [ pytest.param(dict(use_register_v2=True), id="use_register_v2-True"), pytest.param(dict(use_register_v2=False), id="use_register_v2-False"), ] class Nomixin: pass class FastHash(PasswordHash): """Our own 'hasher'. For testing we want a fast hash, but a real one such that the provided password and hash aren't the same (which is what happens when using plaintext). """ name = "fasthash" setting_kwds = () context_kwds = () @classmethod def hash(cls, secret, **kwds): return f"$fh$1${secret}" @classmethod def verify(cls, secret, stored_hash, **context_kwds): new_hash = f"$fh$1${secret}" return new_hash == stored_hash @classmethod def identify(cls, stored_hash): return stored_hash.startswith("$fh$1$") @classmethod def using(cls, relaxed=False, **settings): return type("fasthash2", (cls,), {}) # python 3.13 is strict about not closing sqlite3 db connections. def find_sqlite_connections(): connections = [] for obj in gc.get_objects(): if isinstance(obj, sqlite3.Connection): connections.append(obj) return connections @pytest.fixture() def app(request, pytestconfig): # assert not find_sqlite_connections() # hopefully find tests that don't clean up app = Flask(__name__) app.response_class = Response app.debug = True app.config["SECRET_KEY"] = "secret" app.config["TESTING"] = True app.config["LOGIN_DISABLED"] = False app.config["WTF_CSRF_ENABLED"] = False # Our test emails/domain isn't necessarily valid app.config["SECURITY_EMAIL_VALIDATOR_ARGS"] = {"check_deliverability": False} app.config["SECURITY_TOTP_SECRETS"] = { "1": "TjQ9Qa31VOrfEzuPy4VHQWPCTmRzCnFzMKLxXYiZu9B" } app.config["SECURITY_TOTP_ISSUER"] = "tests" app.config["SECURITY_SMS_SERVICE"] = "test" app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config["SECURITY_PASSWORD_SALT"] = "salty" app.config["SECURITY_CONFIRM_SALT"] = "confirm-salty" # Make this fasthash for most tests - reduces unit test time by 50% app.config["SECURITY_PASSWORD_SCHEMES"] = ["fasthash", "argon2", "bcrypt"] app.config["SECURITY_PASSWORD_HASH"] = "fasthash" app.config["SECURITY_PASSWORD_SINGLE_HASH"] = True register_crypt_handler(FastHash) # Make this hex_md5 for token tests app.config["SECURITY_HASHING_SCHEMES"] = ["hex_md5"] app.config["SECURITY_DEPRECATED_HASHING_SCHEMES"] = [] app.fs_constructor_args = ( dict() ) # allow marks to set items for Security constructor for opt in [ "changeable", "change_email", "change_username", "confirmable", "passwordless", "recoverable", "registerable", "refresh_token", "trackable", "two_factor", "unified_signin", "username_recovery", "webauthn", ]: app.config["SECURITY_" + opt.upper()] = opt in request.keywords marker_getter = request.node.get_closest_marker # Import webauthn, or skip test if webauthn isn't installed webauthn_test = marker_getter("webauthn") if webauthn_test is not None: pytest.importorskip("webauthn") app.fs_constructor_args.update(**webauthn_test.kwargs) oauthlib_test = marker_getter("oauth") if oauthlib_test is not None: pytest.importorskip("authlib") mfa_test = marker_getter("two_factor") or marker_getter("unified_signin") if mfa_test is not None: pytest.importorskip("cryptography") flask_async_test = marker_getter("flask_async") if flask_async_test is not None: pytest.importorskip("asgiref") # from flask[async] # allow parameterized tests to set security config variables if hasattr(request, "param") and isinstance(request.param, Mapping): for key, value in request.param.items(): app.config["SECURITY_" + key.upper()] = value # Override config settings as requested for this test settings = marker_getter("settings") if settings is not None: for key, value in settings.kwargs.items(): app.config["SECURITY_" + key.upper()] = value settings = marker_getter("app_settings") if settings is not None: for key, value in settings.kwargs.items(): app.config[key.upper()] = value # allow pytest command line to override everything if request.config.option.setting: for s in request.config.option.setting: key, value = s.split("=") app.config[key.upper()] = convert_bool_option(value) app.mail = Mail(app) # type: ignore # use babel marker to signify tests that need babel extension. babel = marker_getter("babel") if babel: pytest.importorskip("flask_babel") from flask_babel import Babel if locale := babel.kwargs.get("babel_default_locale", None): app.config["BABEL_DEFAULT_LOCALE"] = locale if babel.kwargs.get("test_xlations", None): i18n_dirname = [ os.path.join(pytestconfig.rootdir, "tests/translations"), ] app.config["SECURITY_I18N_DIRNAME"] = i18n_dirname Babel(app) csrf = marker_getter("csrf") if csrf is not None: # without any keys/arguments - this is the default config # Note that WTF_CSRF_CHECK_DEFAULT = True means Flask_wtf will # run a CSRF check as part of @before_request - before we see it. # Above we set WTF_CSRF_ENABLED to False. With this marker # we want the 'default' config. del app.config["WTF_CSRF_ENABLED"] if "ignore_unauth" in csrf.kwargs.keys(): app.config["WTF_CSRF_CHECK_DEFAULT"] = False app.config["SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS"] = True if "csrfprotect" in csrf.kwargs.keys(): # This is needed when passing CSRF in header or non-form input app.config["WTF_CSRF_CHECK_DEFAULT"] = False CSRFProtect(app) @app.route("/") def index(): return render_template("index.html", content="Home Page") @app.route("/profile") @auth_required() def profile(): if hasattr(app, "security"): if app.security._want_json(flask_request): return jsonify(message="profile") return render_template("index.html", content="Profile Page") @app.route("/post_login") @auth_required() def post_login(): return render_template("index.html", content="Post Login") @app.route("/http", methods=["GET", "POST"]) @http_auth_required def http(): return "HTTP Authentication" @app.route("/http_admin_required") @http_auth_required @permissions_required("admin") def http_admin_required(): assert get_request_attr("fs_authn_via") == "basic" return "HTTP Authentication" @app.route("/http_custom_realm") @http_auth_required("My Realm") def http_custom_realm(): assert get_request_attr("fs_authn_via") == "basic" return render_template("index.html", content="HTTP Authentication") @app.route("/session") @auth_required("session") def session(): return "Session Authentication" @app.route("/token", methods=["GET", "POST"]) @auth_token_required def token(): assert get_request_attr("fs_authn_via") == "token" return render_template("index.html", content="Token Authentication") @app.route("/multi_auth") @auth_required("session", "token", "basic") def multi_auth(): return render_template("index.html", content="Session, Token, Basic auth") @app.route("/post_logout") def post_logout(): return render_template("index.html", content="Post Logout") @app.route("/post_register") def post_register(): return render_template("index.html", content="Post Register") @app.route("/post_confirm") def post_confirm(): return render_template("index.html", content="Post Confirm") @app.route("/post_reset") def post_reset(): return render_template("index.html", content="Post Reset") @app.route("/post_change_username") def post_change_username(): return render_template("index.html", content="Post Change Username") @app.route("/admin") @roles_required("admin") def admin(): assert get_request_attr("fs_authn_via") == "session" return render_template("index.html", content="Admin Page") @app.route("/admin_and_editor") @roles_required("admin", "editor") def admin_and_editor(): return render_template("index.html", content="Admin and Editor Page") @app.route("/admin_or_editor") @roles_accepted("admin", "editor") def admin_or_editor(): return render_template("index.html", content="Admin or Editor Page") @app.route("/simple") @roles_accepted("simple") def simple(): return render_template("index.html", content="SimplePage") @app.route("/admin_perm") @permissions_accepted("full-write", "super") def admin_perm(): return render_template( "index.html", content="Admin Page with full-write or super" ) @app.route("/admin_perm_required") @permissions_required("full-write", "super") def admin_perm_required(): return render_template("index.html", content="Admin Page required") @app.route("/page1") def page_1(): return "Page 1" @app.route("/json", methods=["GET", "POST"]) def echo_json(): return jsonify(flask_request.get_json()) @app.route("/json_auth", methods=["POST"]) @auth_required() def echo_jsonauth(): return jsonify(flask_request.get_json()) @app.route("/unauthz", methods=["GET", "POST"]) def unauthz(): return render_template("index.html", content="Unauthorized") @app.route("/fresh", methods=["GET", "POST"]) @auth_required(within=60) def fresh(): if app.security._want_json(flask_request): return jsonify(title="Fresh Only") else: return render_template("index.html", content="Fresh Only") def revert_forms(): # Some forms/tests have dynamic fields - be sure to revert them. if hasattr(app, "security"): for form_name in [ "login_form", "register_form", "confirm_register_form", "change_username_form", ]: if hasattr(app.security.forms[form_name].cls, "username"): del app.security.forms[form_name].cls.username from flask_security import RegisterFormV2 from flask_security.forms import PasswordConfirmFormMixin, NewPasswordFormMixin for attr in ["username"]: if hasattr(RegisterFormV2, attr): delattr(RegisterFormV2, attr) RegisterFormV2.password_confirm = PasswordConfirmFormMixin.password_confirm RegisterFormV2.password = NewPasswordFormMixin.password request.addfinalizer(revert_forms) yield app @pytest.fixture() def outbox(app): with app.mail.record_messages() as outbox: yield outbox @pytest.fixture() def mongoengine_datastore(app, tmpdir, realmongodburl): ds, td = mongoengine_setup(app, tmpdir, realmongodburl) yield ds td() def mongoengine_setup(app, tmpdir, realmongodburl): # To run against a realdb: mongod --dbpath import pymongo import mongomock from mongoengine import Document, connect from mongoengine.fields import ( BinaryField, BooleanField, DateTimeField, IntField, ListField, ReferenceField, StringField, ) from mongoengine import PULL, CASCADE, disconnect_all db_name = "flask_security_test" db_host = realmongodburl if realmongodburl else "mongodb://localhost" db_client_class = pymongo.MongoClient if realmongodburl else mongomock.MongoClient db = connect( alias=db_name, db=db_name, host=db_host, port=27017, mongo_client_class=db_client_class, ) class Role(Document, RoleMixin): name = StringField(required=True, unique=True, max_length=80) description = StringField(max_length=255) permissions = ListField(required=False) meta = {"db_alias": db_name} class WebAuthn(Document, WebAuthnMixin): credential_id = BinaryField(primary_key=True, max_bytes=1024, required=True) public_key = BinaryField(required=True) sign_count = IntField(default=0) transports = ListField(required=False) backup_state = BooleanField(required=True) device_type = StringField(max_length=64, required=True) # a JSON string as returned from registration extensions = StringField(max_length=255) lastuse_datetime = DateTimeField(required=True) # name is provided by user - we make sure it is unique per user name = StringField(max_length=64, required=True) usage = StringField(max_length=64, required=True) # we need to be able to look up a user from a credential_id user = ReferenceField("User") meta = {"db_alias": db_name} def get_user_mapping(self) -> dict[str, str]: """ Return the mapping from webauthn back to User """ return dict(id=self.user.id) class FsRefreshTracker(Document, RefreshTrackerMixin): """Refresh Token Tracker""" refresh_family = StringField(max_length=64, required=True) gen = IntField(default=1) expires_at = DateTimeField(required=True) revoked_at = DateTimeField(required=False) last_used_at = DateTimeField(required=True) name = StringField(max_length=64) user = ReferenceField("User") meta = {"db_alias": db_name} class User(Document, UserMixin): email = StringField(unique=True, max_length=255) fs_uniquifier = StringField(unique=True, max_length=64, required=True) fs_webauthn_user_handle = StringField(unique=True, max_length=64) username = StringField(unique=True, required=False, sparse=True, max_length=255) password = StringField(required=False, max_length=255) security_number = IntField(unique=True, required=False, sparse=True) last_login_at = DateTimeField() current_login_at = DateTimeField() tf_primary_method = StringField(max_length=255) tf_totp_secret = StringField(max_length=255) tf_phone_number = StringField(max_length=255) mf_recovery_codes = ListField(required=False) us_totp_secrets = StringField() us_phone_number = StringField( max_length=255, unique=True, required=False, sparse=True ) last_login_ip = StringField(max_length=100) current_login_ip = StringField(max_length=100) login_count = IntField() active = BooleanField(default=True) confirmed_at = DateTimeField() roles = ListField(ReferenceField(Role), default=[]) webauthn = ListField( ReferenceField(WebAuthn, reverse_delete_rule=PULL), default=[] ) refresh_trackers = ListField( ReferenceField(FsRefreshTracker, reverse_delete_rule=PULL), default=[] ) meta = {"db_alias": db_name} def get_security_payload(self): return {"email": str(self.email)} User.register_delete_rule(WebAuthn, "user", CASCADE) User.register_delete_rule(FsRefreshTracker, "user", CASCADE) def tear_down(): with app.app_context(): User.drop_collection() Role.drop_collection() WebAuthn.drop_collection() db.drop_database(db_name) disconnect_all() return ( MongoEngineUserDatastore(db, User, Role, WebAuthn, FsRefreshTracker), tear_down, ) @pytest.fixture() def sqlalchemy_datastore(app, tmpdir, realdburl): ds, td = sqlalchemy_setup(app, tmpdir, realdburl) yield ds td() def sqlalchemy_setup(app, tmpdir, realdburl): pytest.importorskip("flask_sqlalchemy") from flask_sqlalchemy import SQLAlchemy from sqlalchemy import Column, Integer from flask_security.models import fsqla_v4 as fsqla if realdburl: db_url, db_info = _setup_realdb(realdburl) app.config["SQLALCHEMY_DATABASE_URI"] = db_url else: app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" # In Flask-SQLAlchemy >= 3.0.0 queries are no longer logged automatically, # even in debug or testing mode. app.config["SQLALCHEMY_RECORD_QUERIES"] = True db = SQLAlchemy(app) fsqla.FsModels.set_db_info(db) class Role(db.Model, fsqla.FsRoleMixin): pass class WebAuthn(db.Model, fsqla.FsWebAuthnMixin): pass class FsRefreshTracker(db.Model, fsqla.FsRefreshTrackerMixin): pass class User( db.Model, fsqla.FsUserMixin, app.config.get("TESTING_USER_MIXIN", Nomixin) ): security_number = Column(Integer, unique=True) def __init__(self, *args, **kwargs): from flask import current_app super().__init__(*args, **kwargs) inject = current_app.config.get("TESTING_USER_INJECT") or dict() for k, v in inject.items(): setattr(User, k, v) def get_security_payload(self): # Make sure we still properly hook up to flask's JSON extension # which handles datetime return {"email": str(self.email), "last_update": self.update_datetime} def augment_auth_token(self, tdata): # for testing - if TESTING_AUGMENT_AUTH_TOKEN is set - call that from flask import current_app if cb := current_app.config.get("TESTING_AUGMENT_AUTH_TOKEN"): cb(tdata) with app.app_context(): db.create_all() def tear_down(): with app.app_context(): if realdburl: db.drop_all() db.engine.dispose() # make sure Flask-SQLAlchemy connections are closed _teardown_realdb(db_info) engine = db.engine engine.dispose() return ( SQLAlchemyUserDatastore(db, User, Role, WebAuthn, FsRefreshTracker), tear_down, ) @pytest.fixture() def fsqlalite_datastore(app, tmpdir, realdburl): ds, td = fsqlalite_setup(app, tmpdir, realdburl) yield ds td() @pytest.fixture() def fsqlalite_min_datastore(app, tmpdir, realdburl): pytest.importorskip("flask_sqlalchemy_lite") from sqlalchemy.orm import declared_attr, mapped_column, relationship from sqlalchemy import String class FsMinUserMixin(UserMixin): # flask_security basic fields id: Mapped[int] = mapped_column(primary_key=True) # type: ignore email: Mapped[str] = mapped_column(String(255), unique=True) # type: ignore password: Mapped[str | None] = mapped_column(String(255)) # type: ignore active: Mapped[bool] = mapped_column() # type: ignore fs_uniquifier: Mapped[str] = mapped_column( # type: ignore String(64), unique=True ) @declared_attr def roles(cls): # The first arg is a class name, the backref is a column name return relationship( "Role", secondary="roles_users", back_populates="users", ) ds, td = fsqlalite_setup( app, tmpdir, realdburl, usermixin=FsMinUserMixin, use_webauthn=False, use_fs_tracker=False, ) yield ds td() def fsqlalite_setup( app, tmpdir, realdburl, usermixin=None, use_webauthn=True, use_fs_tracker=True ): pytest.importorskip("flask_sqlalchemy_lite") from flask_sqlalchemy_lite import SQLAlchemy from sqlalchemy.orm import DeclarativeBase, mapped_column from flask_security.models import sqla_v2 as sqla if not usermixin: usermixin = sqla.FsUserMixin if realdburl: db_url, db_info = _setup_realdb(realdburl) else: db_url = "sqlite:///:memory:" app.config |= { "SQLALCHEMY_ENGINES": { "default": {"url": db_url, "pool_pre_ping": True}, }, } db = SQLAlchemy(app) class Model(DeclarativeBase): pass sqla.FsModels.set_db_info(base_model=Model) class Role(Model, sqla.FsRoleMixin): __tablename__ = "role" if use_webauthn: class WebAuthn(Model, sqla.FsWebAuthnMixin): __tablename__ = "webauthn" if use_fs_tracker: class FsRefreshTracker(Model, sqla.FsRefreshTrackerMixin): __tablename__ = "fs_refresh_tracker" class User(Model, usermixin, app.config.get("TESTING_USER_MIXIN", Nomixin)): __tablename__ = "user" security_number: Mapped[t.Optional[int]] = mapped_column( # type: ignore unique=True ) def get_security_payload(self) -> dict[str, t.Any]: # Make sure we still properly hook up to flask's JSON extension # which handles datetime return {"email": str(self.email), "last_update": self.update_datetime} with app.app_context(): Model.metadata.create_all(db.engine) def tear_down(): with app.app_context(): Model.metadata.drop_all(db.engine) engine = db.engine engine.dispose() if realdburl: _teardown_realdb(db_info) return ( FSQLALiteUserDatastore( db, User, Role, WebAuthn if use_webauthn else None, FsRefreshTracker if use_fs_tracker else None, ), tear_down, ) @pytest.fixture() def sqlalchemy_session_datastore(app, tmpdir, realdburl): if sys.version_info < (3, 10): pytest.skip("requires python3.10 or higher") ds, td = sqlalchemy_session_setup(app, tmpdir, realdburl) yield ds td() def sqlalchemy_session_setup(app, tmpdir, realdburl, **engine_kwargs): """ Note that we test having a different user id column name here. """ pytest.importorskip("sqlalchemy") from sqlalchemy import create_engine from sqlalchemy.orm import ( mapped_column, scoped_session, sessionmaker, declarative_base, ) from sqlalchemy.ext.declarative import declared_attr from sqlalchemy import ( Column, Integer, ForeignKey, ) from flask_security.models import sqla_v2 as sqla if realdburl: db_url, db_info = _setup_realdb(realdburl) engine = db_info["engine"] else: db_url = "sqlite:///:memory:" engine = create_engine(db_url, **engine_kwargs) db_session = scoped_session( sessionmaker(autocommit=False, autoflush=False, bind=engine) ) app.teardown_appcontext(lambda exc: db_session.close()) Base = declarative_base() # Note that in this case we don't call set_db_info since we are using # normal table names AND we need our own RolesUsers table since we modified the # PK names. class WebAuthn(Base, sqla.FsWebAuthnMixin): __tablename__ = "webauthn" @declared_attr def user_id(self) -> Mapped[int]: return mapped_column(ForeignKey("user.myuserid", ondelete="CASCADE")) def get_user_mapping(self) -> dict[str, t.Any]: """ Return the filter needed by find_user() to get the user associated with this webauthn credential. """ return dict(myuserid=self.user_id) class FsRefreshTracker(Base, sqla.FsRefreshTrackerMixin): __tablename__ = "fs_refresh_tracker" @declared_attr def user_id(self) -> Mapped[int]: return mapped_column(ForeignKey("user.myuserid", ondelete="CASCADE")) class RolesUsers(Base): __tablename__ = "roles_users" id = Column(Integer(), primary_key=True) user_id = Column("user_id", Integer(), ForeignKey("user.myuserid")) role_id = Column("role_id", Integer(), ForeignKey("role.myroleid")) class Role(Base, sqla.FsRoleMixin): __tablename__ = "role" myroleid: Mapped[int] = mapped_column(primary_key=True) # type: ignore id: Mapped[int] = mapped_column(nullable=True) # type: ignore class User(Base, sqla.FsUserMixin, app.config.get("TESTING_USER_MIXIN", Nomixin)): __tablename__ = "user" myuserid: Mapped[int] = mapped_column(primary_key=True) # type: ignore id: Mapped[int] = mapped_column(nullable=True) # type: ignore security_number: Mapped[t.Optional[int]] = mapped_column( # type: ignore unique=True ) def get_security_payload(self): # Make sure we still properly hook up to flask's JSON extension # which handles datetime return {"email": str(self.email), "last_update": self.update_datetime} with app.app_context(): Base.metadata.create_all(bind=engine) def tear_down(): with app.app_context(): Base.metadata.drop_all(bind=engine) engine.dispose() if realdburl: _teardown_realdb(db_info) return ( SQLAlchemySessionUserDatastore( db_session, User, Role, WebAuthn, FsRefreshTracker ), tear_down, ) @pytest.fixture() def peewee_datastore(app, tmpdir, realdburl): ds, td = peewee_setup(app, tmpdir, realdburl) yield ds td() def peewee_setup(app, tmpdir, realdburl): pytest.importorskip("peewee") from peewee import ( TextField, DateTimeField, Field, IntegerField, BooleanField, BlobField, ForeignKeyField, CharField, ) from playhouse.flask_utils import FlaskDB if realdburl: engine_mapper = { "postgresql": "peewee.PostgresqlDatabase", "mysql": "peewee.MySQLDatabase", } db_url, db_info = _setup_realdb(realdburl) pieces = urlsplit(db_url) db_config = { "name": pieces.path[1:], "engine": engine_mapper[pieces.scheme.split("+")[0]], "user": pieces.username, "password": pieces.password, "host": pieces.hostname, "port": pieces.port, } else: f, path = tempfile.mkstemp( prefix="flask-security-test-db", suffix=".db", dir=str(tmpdir) ) db_config = {"name": path, "engine": "peewee.SqliteDatabase"} app.config["DATABASE"] = db_config db = FlaskDB(app) class AsaList(Field): field_type = "text" def db_value(self, value): try: return ",".join(value) except TypeError: return value def python_value(self, value): if value: return value.split(",") return [] class Role(RoleMixin, db.Model): name = CharField(unique=True, max_length=80) description = TextField(null=True) permissions = AsaList(null=True) class User(UserMixin, db.Model): email = TextField(unique=True, null=False) fs_uniquifier = TextField(unique=True, null=False) fs_webauthn_user_handle = TextField(unique=True, null=True) username = TextField(unique=True, null=True) security_number = IntegerField(null=True) password = TextField(null=True) last_login_at = DateTimeField(null=True) current_login_at = DateTimeField(null=True) tf_primary_method = TextField(null=True) tf_totp_secret = TextField(null=True) tf_phone_number = TextField(null=True) mf_recovery_codes = AsaList(null=True) us_totp_secrets = TextField(null=True) us_phone_number = TextField(null=True, unique=True) last_login_ip = TextField(null=True) current_login_ip = TextField(null=True) login_count = IntegerField(null=True) active = BooleanField(default=True) confirmed_at = DateTimeField(null=True) def get_security_payload(self): return {"email": str(self.email)} class WebAuthn(WebAuthnMixin, db.Model): credential_id = BlobField(unique=True, null=False, index=True) public_key = BlobField(null=False) sign_count = IntegerField(default=0) transports = AsaList(null=True) # a JSON string as returned from registration extensions = TextField(null=True) lastuse_datetime = DateTimeField(null=False) # name is provided by user - we make sure is unique per user name = TextField(null=False) usage = TextField(null=False) backup_state = BooleanField() device_type = TextField(null=False) # This creates a real column called user_id user = ForeignKeyField(User, backref="webauthn") class FsRefreshTracker(RefreshTrackerMixin, db.Model): """Refresh Token Tracker""" refresh_family = TextField(unique=True, null=False) gen = IntegerField(default=1) expires_at = DateTimeField(null=False) revoked_at = DateTimeField(null=True) last_used_at = DateTimeField(null=False) name = TextField(null=False) user = ForeignKeyField(User, backref="refresh_trackers") class UserRoles(db.Model): """Peewee does not have built-in many-to-many support, so we have to create this mapping class to link users to roles.""" user = ForeignKeyField(User, backref="roles") role = ForeignKeyField(Role, backref="users") name = property(lambda self: self.role.name) description = property(lambda self: self.role.description) def get_permissions(self): return self.role.get_permissions() with app.app_context(): for Model in (Role, User, UserRoles, WebAuthn, FsRefreshTracker): Model.drop_table() Model.create_table() def tear_down(): if realdburl: db.close_db(None) _teardown_realdb(db_info) else: db.close_db(None) os.close(f) os.remove(path) return ( PeeweeUserDatastore(db, User, Role, UserRoles, WebAuthn, FsRefreshTracker), tear_down, ) @pytest.fixture() def client(request, app, sqlalchemy_datastore): app.security = Security( app, datastore=sqlalchemy_datastore, **app.fs_constructor_args ) populate_data(app) return app.test_client(use_cookies=not app.config.get("TESTING_NO_COOKIES", False)) @pytest.fixture() def client_nc(request, app, sqlalchemy_datastore): # useful for testing token auth. # No Cookies for You! app.security = Security(app, datastore=sqlalchemy_datastore) populate_data(app) return app.test_client(use_cookies=False) @pytest.fixture() def signals(): """Tracks signal calls for testing purposes. This sets up a receiver for all signals and tracks the data """ from flask_security import signals import inspect import blinker signal_calls = dict() def_signals = inspect.getmembers( signals, lambda v: isinstance(v, blinker.NamedSignal) ) for name, sig in def_signals: sc = signal_calls[name] = [] def _on(app, _sc=sc, **kwargs): assert isinstance(app, Flask) kwargs["request_endpoint"] = ( flask_request.endpoint if flask_request else None ) # user argument is an ORM structure so may not be available in a test # outside of the request - capture value(s) here user = kwargs.get("user", None) if user: assert isinstance(kwargs["user"], UserMixin) kwargs["user_email"] = user.email _sc.append(kwargs) sig.connect(_on, weak=False) return signal_calls @pytest.fixture( params=[ "cl-fsqlalchemy", "cl-sqla-session", "cl-mongo", "cl-peewee", "cl-fsqlalite", ] ) def clients(request, app, tmpdir, realdburl, realmongodburl): if request.param == "cl-fsqlalchemy": ds, td = sqlalchemy_setup(app, tmpdir, realdburl) elif request.param == "cl-sqla-session": if sys.version_info < (3, 10): pytest.skip("requires python3.10 or higher") ds, td = sqlalchemy_session_setup(app, tmpdir, realdburl) elif request.param == "cl-mongo": ds, td = mongoengine_setup(app, tmpdir, realmongodburl) elif request.param == "cl-peewee": ds, td = peewee_setup(app, tmpdir, realdburl) elif request.param == "cl-fsqlalite": ds, td = fsqlalite_setup(app, tmpdir, realdburl) app.security = Security(app, datastore=ds, **app.fs_constructor_args) populate_data(app) if request.param == "cl-peewee": # peewee is insistent on a single connection? ds.db.close_db(None) yield app.test_client(use_cookies=not app.config.get("TESTING_NO_COOKIES", False)) td() @pytest.fixture() def in_app_context(request, app, sqlalchemy_datastore): app.security = Security( app, datastore=sqlalchemy_datastore, **app.fs_constructor_args ) with app.app_context(): yield app @pytest.fixture() def get_message(app: Flask) -> t.Callable[..., bytes]: def fn(key, **kwargs): rv = app.config["SECURITY_MSG_" + key][0] % kwargs return rv.encode("utf-8") return fn @pytest.fixture() def get_message_local(app): def fn(key, **kwargs): return localize_callback(app.config["SECURITY_MSG_" + key][0], **kwargs) return fn @pytest.fixture( params=[ "sqlalchemy", "sqlalchemy-session", "mongoengine", "peewee", "fsqlalite", ] ) def datastore(request, app, tmpdir, realdburl, realmongodburl): if request.param == "sqlalchemy": ds, td = sqlalchemy_setup(app, tmpdir, realdburl) elif request.param == "sqlalchemy-session": ds, td = sqlalchemy_session_setup(app, tmpdir, realdburl) elif request.param == "mongoengine": ds, td = mongoengine_setup(app, tmpdir, realmongodburl) elif request.param == "peewee": ds, td = peewee_setup(app, tmpdir, realdburl) elif request.param == "fsqlalite": ds, td = fsqlalite_setup(app, tmpdir, realdburl) yield ds td() @pytest.fixture() def script_info(app, datastore): from flask.cli import ScriptInfo def create_app(): uia = [ {"email": {"mapper": uia_email_mapper}}, {"us_phone_number": {"mapper": lambda x: x}}, ] app.config.update(**{"SECURITY_USER_IDENTITY_ATTRIBUTES": uia}) app.security = Security(app, datastore=datastore) return app return ScriptInfo(create_app=create_app) @pytest.fixture() def script_info_min(app, fsqlalite_min_datastore): from flask.cli import ScriptInfo def create_app(): app.security = Security(app, datastore=fsqlalite_min_datastore) return app return ScriptInfo(create_app=create_app) def pytest_addoption(parser): parser.addoption( "--realdburl", action="store", default=None, help="""Set url for using real database for testing. For postgres: 'postgresql://user:password@host/')""", ) parser.addoption( "--realmongodburl", action="store", default=None, help="""Set url for using real mongo database for testing. e.g. 'localhost'""", ) parser.addoption( "--setting", default=None, action="append", help="""Set one or more SECURITY_ settings from command line. e.g. --setting anonymous_user_enable=False""", ) @pytest.fixture(scope="session") def realdburl(request): """ Support running datastore tests against a real DB. For example psycopg2 is very strict about types in queries compared to sqlite To use postgres you need to of course run a postgres instance on localhost then pass in an extra arg to pytest: --realdburl postgresql://@localhost/ For mysql same - just download and add a root password. --realdburl "mysql+pymysql://root:@localhost/" """ return request.config.option.realdburl @pytest.fixture(scope="session") def realmongodburl(request): """ Support running datastore tests against a real Mongo DB. --realmongodburl "localhost" """ return request.config.option.realmongodburl def _setup_realdb(realdburl): """ Called when we want to run unit tests against a real DB. This is useful since different DB drivers are pickier about queries etc. (such as pyscopg2 and postgres) """ from sqlalchemy import create_engine from sqlalchemy_utils import database_exists, create_database db_name = "flask_security_test_%s" % str(time.time()).replace(".", "_") db_uri = realdburl + db_name engine = create_engine(db_uri) if not database_exists(engine.url): create_database(engine.url) print("Setting up real DB at " + db_uri) return db_uri, {"engine": engine} def _teardown_realdb(db_info): from sqlalchemy_utils import drop_database drop_database(db_info["engine"].url) @pytest.fixture() def humanizer(request): pytest.importorskip("humanize") import humanize if getattr(request, "param", None): humanize.i18n.activate(request.param) yield 1 humanize.i18n.deactivate() else: yield 0 pallets-eco-flask-security-72bf929/tests/templates/000077500000000000000000000000001525721743500224065ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/tests/templates/_messages.html000066400000000000000000000004101525721743500252350ustar00rootroot00000000000000{%- with messages = get_flashed_messages(with_categories=true) -%} {% if messages %}
    {% for category, message in messages %}
  • {{ message }}
  • {% endfor %}
{% endif %} {%- endwith %} pallets-eco-flask-security-72bf929/tests/templates/_nav.html000066400000000000000000000014361525721743500242230ustar00rootroot00000000000000{%- if _fs_is_user_authenticated(current_user) -%}

{{ _fsdomain('Welcome') }} {{ current_user.calc_username() }}

{%- endif %}
  • Index
  • Profile
  • {% if current_user and current_user.has_role('admin') -%}
  • Admin
  • {% endif -%} {% if current_user and (current_user.has_role('admin') or current_user.has_role('editor')) -%}
  • Admin or Editor
  • {% endif -%}
  • {%- if _fs_is_user_authenticated(current_user) -%} Log out {%- else -%} Log in {%- endif -%}
pallets-eco-flask-security-72bf929/tests/templates/custom_security/000077500000000000000000000000001525721743500256475ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/tests/templates/custom_security/change_email.html000066400000000000000000000000531525721743500311270ustar00rootroot00000000000000CUSTOM CHANGE EMAIL {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/change_password.html000066400000000000000000000000561525721743500317050ustar00rootroot00000000000000CUSTOM CHANGE PASSWORD {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/change_username.html000066400000000000000000000000561525721743500316620ustar00rootroot00000000000000CUSTOM CHANGE USERNAME {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/forgot_password.html000066400000000000000000000000561525721743500317600ustar00rootroot00000000000000CUSTOM FORGOT PASSWORD {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/login_user.html000066400000000000000000000000511525721743500306770ustar00rootroot00000000000000CUSTOM LOGIN USER {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/logout_user.html000066400000000000000000000000521525721743500311010ustar00rootroot00000000000000CUSTOM LOGOUT USER {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/mf_recovery.html000066400000000000000000000000521525721743500310520ustar00rootroot00000000000000CUSTOM MF RECOVERY {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/mf_recovery_codes.html000066400000000000000000000000601525721743500322260ustar00rootroot00000000000000CUSTOM MF RECOVERY CODES {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/recover_username.html000066400000000000000000000000571525721743500321030ustar00rootroot00000000000000CUSTOM RECOVER USERNAME {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/register_user.html000066400000000000000000000000541525721743500314160ustar00rootroot00000000000000CUSTOM REGISTER USER {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/reset_password.html000066400000000000000000000000551525721743500316010ustar00rootroot00000000000000CUSTOM RESET PASSWORD {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/send_confirmation.html000066400000000000000000000000601525721743500322320ustar00rootroot00000000000000CUSTOM SEND CONFIRMATION {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/send_login.html000066400000000000000000000000511525721743500306520ustar00rootroot00000000000000CUSTOM SEND LOGIN {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/tf_setup.html000066400000000000000000000000571525721743500303700ustar00rootroot00000000000000CUSTOM TWO FACTOR SETUP {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/tf_verify.html000066400000000000000000000000651525721743500305330ustar00rootroot00000000000000CUSTOM TWO FACTOR VERIFY CODE {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/us_setup.html000066400000000000000000000000631525721743500304030ustar00rootroot00000000000000CUSTOM UNIFIED SIGNIN SETUP {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/us_signin.html000066400000000000000000000000561525721743500305340ustar00rootroot00000000000000CUSTOM UNIFIED SIGN IN {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/us_verify.html000066400000000000000000000000551525721743500305500ustar00rootroot00000000000000CUSTOM UNIFIED VERIFY {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/verify.html000066400000000000000000000000521525721743500300360ustar00rootroot00000000000000CUSTOM VERIFY USER {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/wan_register.html000066400000000000000000000000531525721743500312240ustar00rootroot00000000000000CUSTOM WAN REGISTER {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/wan_signin.html000066400000000000000000000000511525721743500306650ustar00rootroot00000000000000CUSTOM WAN SIGNIN {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/custom_security/wan_verify.html000066400000000000000000000000511525721743500307020ustar00rootroot00000000000000CUSTOM WAN VERIFY {{ global }} {{ foo }} pallets-eco-flask-security-72bf929/tests/templates/generic_confirm.html000066400000000000000000000004741525721743500264320ustar00rootroot00000000000000{% include "security/_messages.html" %} {% from "security/_macros.html" import render_field_with_errors, render_field, render_form_errors %} {{ render_field_with_errors(send_confirmation_form.email) }} {{ render_field_with_errors(send_confirmation_form.recaptcha) }} {{ render_form_errors(send_confirmation_form) }} pallets-eco-flask-security-72bf929/tests/templates/generic_reset.html000066400000000000000000000004661525721743500261200ustar00rootroot00000000000000{% include "security/_messages.html" %} {% from "security/_macros.html" import render_field_with_errors, render_field, render_form_errors %} {{ render_field_with_errors(forgot_password_form.email) }} {{ render_field_with_errors(forgot_password_form.recaptcha) }} {{ render_form_errors(forgot_password_form) }} pallets-eco-flask-security-72bf929/tests/templates/index.html000066400000000000000000000001161525721743500244010ustar00rootroot00000000000000{% include "_messages.html" %} {% include "_nav.html" %}

{{ content }}

pallets-eco-flask-security-72bf929/tests/templates/register.html000066400000000000000000000010051525721743500251140ustar00rootroot00000000000000{% include "_messages.html" %} {% include "_nav.html" %}

Register

{{ register_user_form.hidden_tag() }} {{ register_user_form.email.label }} {{ register_user_form.email }}
{{ register_user_form.password.label }} {{ register_user_form.password }}
{{ register_user_form.password_confirm.label }} {{ register_user_form.password_confirm }}
{{ register_user_form.submit }}

{{ content }}

pallets-eco-flask-security-72bf929/tests/templates/security/000077500000000000000000000000001525721743500242555ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/tests/templates/security/email/000077500000000000000000000000001525721743500253445ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/tests/templates/security/email/change_email_instructions_test.txt000066400000000000000000000010361525721743500343640ustar00rootroot00000000000000{# This template receives the following context: link - the link that should be fetched (GET) to confirm token - this token is part of confirmation link - but can be used to construct arbitrary URLs for redirecting. user - the entire user model object within - formatted expiration of link security - the Flask-Security configuration #} Link:{{ link }} Email:{{ user.email }} Token:{{ token }} RegisterBlueprint:{{ security.register_blueprint }} Within:{{ within }} {% include "security/email/change_email_instructions.txt" %} pallets-eco-flask-security-72bf929/tests/templates/security/email/confirmation_instructions_test.txt000066400000000000000000000011221525721743500344540ustar00rootroot00000000000000{# This template receives the following context: confirmation_link - the link that should be fetched (GET) to confirm confirmation_token - this token is part of confirmation link - but can be used to construct arbitrary URLs for redirecting. user - the entire user model object within - formatted expiration of link security - the Flask-Security configuration #} Link:{{ confirmation_link }} Email:{{ user.email }} Token:{{ confirmation_token }} RegisterBlueprint:{{ security.register_blueprint }} Within:{{ within }} {% include "security/email/confirmation_instructions.txt" %} pallets-eco-flask-security-72bf929/tests/templates/security/email/login_instructions.txt000066400000000000000000000001711525721743500320400ustar00rootroot00000000000000Link:{{ login_link }} Email:{{ user.email }} Token:{{ login_token }} RegisterBlueprint:{{ security.register_blueprint }} pallets-eco-flask-security-72bf929/tests/templates/security/email/reset_instructions_test.txt000066400000000000000000000011601525721743500331100ustar00rootroot00000000000000{# This template receives the following context: reset_link - the link that should be fetched (GET) to reset reset_token - this token is part of reset link - but can be used to construct arbitrary URLs for redirecting. user - the entire user model object within - formatted expiration of link security - the Flask-Security configuration #} {{ global }} {{ foo }} Link:{{ reset_link }} Email:{{ user.email }} Token:{{ reset_token }} RegisterBlueprint:{{ security.register_blueprint }} SecurityConfig:{{ config["SECURITY_RESET_URL"] }} Within:{{ within }} {% include "security/email/reset_instructions.txt" %} pallets-eco-flask-security-72bf929/tests/templates/security/email/us_instructions_test.txt000066400000000000000000000011211525721743500324120ustar00rootroot00000000000000{# This template receives the following context: login_link - the link that should be fetched (GET) to reset login_token - this token is part of reset link - but can be used to construct arbitrary URLs for redirecting. user - the entire user model object username - username within - formatted expiration of link security - the Flask-Security configuration #} Link:{{ login_link }} Email:{{ user.email }} Token:{{ login_token }} RegisterBlueprint:{{ security.register_blueprint }} Username:{{ username }} Within:{{ within }} {% include "security/email/us_instructions.txt" %} pallets-eco-flask-security-72bf929/tests/templates/security/email/welcome.txt000066400000000000000000000007571525721743500275510ustar00rootroot00000000000000{# This template receives the following context: confirmation_link - the link that should be fetched (GET) to confirm user - the entire user model object confirmation_token - this token is part of confirmation link - but can be used to construct arbitrary URLs for redirecting. security - the Flask-Security configuration #} Link:{{ confirmation_link }} Email:{{ user.email }} Token:{{ confirmation_token }} RegisterBlueprint:{{ security.register_blueprint }} ExtraContext:{{ foo }} pallets-eco-flask-security-72bf929/tests/templates/security/email/welcome_existing.txt000066400000000000000000000014731525721743500314570ustar00rootroot00000000000000{# This template receives the following context: user - the entire user model object security - the Flask-Security configuration recovery_link - if enabled. reset_link - reset link if enabled reset_token - this token is part of reset link - but can be used to construct arbitrary URLs for redirecting. confirmation_link - confirmation link is user not yet confirmed (and enabled) confirmation_token This template is used when returning generic responses and don't/can't provide detailed errors as part of form validation to avoid email/username enumeration. #} Email:{{ user.email }} User:{{ user.username }} RegisterBlueprint:{{ security.register_blueprint }} ResetLink:{{ reset_link }} ResetToken:{{ reset_token }} ConfirmationLink:{{ confirmation_link }} ConfirmationToken:{{ confirmation_token }} pallets-eco-flask-security-72bf929/tests/test_async.py000066400000000000000000000072071525721743500231440ustar00rootroot00000000000000""" test_async ~~~~~~~~~~ Tests using Flask async. Make sure our decorators allow for async views Make sure signal receivers can be async :copyright: (c) 2023-2023 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import asyncio import base64 import pytest from flask_principal import identity_changed from flask_security import ( anonymous_user_required, auth_token_required, auth_required, http_auth_required, roles_required, roles_accepted, permissions_required, permissions_accepted, unauth_csrf, ) from tests.test_utils import ( authenticate, json_authenticate, ) pytestmark = pytest.mark.flask_async() def test_auth_required(app, client): @app.route("/async_test") @auth_required() async def async_test(): await asyncio.sleep(0) return "Access Granted" authenticate(client) response = client.get("/async_test") assert b"Access Granted" in response.data def test_auth_token_required(app, client): @app.route("/async_test") @auth_token_required async def async_test(): await asyncio.sleep(0) return "Access Granted" @identity_changed.connect_via(app) async def ic(myapp, identity, **extra_args): await asyncio.sleep(0) response = json_authenticate(client) token = response.json["response"]["user"]["authentication_token"] response = client.get("/async_test?auth_token=" + token) assert b"Access Granted" in response.data def test_auth_http_required(app, client): @app.route("/async_test") @http_auth_required async def async_test(): await asyncio.sleep(0) return "Access Granted" response = client.get( "/async_test", headers={ "Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:password").decode("utf-8") }, ) assert b"Access Granted" in response.data def test_roles_required(app, client): @app.route("/async_test") @roles_required("admin") async def async_test(): await asyncio.sleep(0) return "Access Granted" authenticate(client) response = client.get("/async_test") assert b"Access Granted" in response.data def test_roles_accepted(app, client): @app.route("/async_test") @roles_accepted("admin") async def async_test(): await asyncio.sleep(0) return "Access Granted" authenticate(client) response = client.get("/async_test") assert b"Access Granted" in response.data def test_permissions_required(app, client): @app.route("/async_test") @permissions_required("super") async def async_test(): await asyncio.sleep(0) return "Access Granted" authenticate(client) response = client.get("/async_test") assert b"Access Granted" in response.data def test_permissions_accepted(app, client): @app.route("/async_test") @permissions_accepted("super") async def async_test(): await asyncio.sleep(0) return "Access Granted" authenticate(client) response = client.get("/async_test") assert b"Access Granted" in response.data def test_anon(app, client): @app.route("/async_test") @anonymous_user_required async def async_test(): await asyncio.sleep(0) return "Access Granted" response = client.get("/async_test") assert b"Access Granted" in response.data def test_unauth_csrf(app, client): @app.route("/async_test") @unauth_csrf() async def async_test(): await asyncio.sleep(0) return "Access Granted" response = client.get("/async_test") assert b"Access Granted" in response.data pallets-eco-flask-security-72bf929/tests/test_basic.py000066400000000000000000001412071525721743500231070ustar00rootroot00000000000000""" test_basic ~~~~~~~~~~~ Test common functionality :copyright: (c) 2019-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import base64 import json import re import pytest from flask import Blueprint, g from flask_security import uia_email_mapper from flask_security.decorators import auth_required from flask_principal import identity_loaded from tests.conftest import v2_param from tests.test_utils import ( authenticate, capture_flashes, capture_queries, check_location, get_auth_token_version_3x, get_form_action, get_form_input_value, hash_password, init_app_with_options, is_authenticated, json_authenticate, logout, populate_data, verify_token, get_existing_session, ) def test_login_view(client): response = client.get("/login") assert b"

Login

" in response.data def test_authenticate(client): response = authenticate(client) assert response.status_code in [302, 303] response = authenticate(client, follow_redirects=True) assert b"Welcome matt@lp.com" in response.data @pytest.mark.settings(anonymous_user_disabled=True) def test_authenticate_no_anon(client): response = authenticate(client) assert response.status_code in [302, 303] response = authenticate(client, follow_redirects=True) assert b"Welcome matt@lp.com" in response.data def test_authenticate_with_next(client): data = dict(email="matt@lp.com", password="password") response = client.post("/login?next=/page1", data=data, follow_redirects=True) assert b"Page 1" in response.data def test_authenticate_with_next_bp(app, client): api = Blueprint("api", __name__) @api.route("/info") def info(): pass app.register_blueprint(api, url_prefix="/api") data = dict(email="matt@lp.com", password="password") response = client.post("/login?next=api.info", data=data, follow_redirects=False) assert response.status_code in [302, 303] assert "api/info" in response.location def test_authenticate_with_invalid_next(client, get_message): data = dict(email="matt@lp.com", password="password") response = client.post("/login?next=http://google.com", data=data) assert get_message("INVALID_REDIRECT") in response.data @pytest.mark.settings(flash_messages=False) def test_authenticate_with_invalid_next_json(client, get_message): data = dict(email="matt@lp.com", password="password") response = client.post("/login?next=http://google.com", json=data) assert response.json["response"]["errors"][0].encode() == get_message( "INVALID_REDIRECT" ) def test_authenticate_with_invalid_malformed_next(client, get_message): data = dict(email="matt@lp.com", password="password") response = client.post("/login?next=http:///google.com", data=data) assert get_message("INVALID_REDIRECT") in response.data def test_unauthenticated(app, client, get_message): from flask_security import user_unauthenticated from flask import request recvd = [] @user_unauthenticated.connect_via(app) def un(myapp, **extra): assert request.path == "/profile" recvd.append("gotit") response = client.get("/profile", follow_redirects=False) assert len(recvd) == 1 assert response.location == "/login?next=/profile" @pytest.mark.flask_async() def test_unauthenticated_async(app, client, get_message): from flask_security import user_unauthenticated from flask import request recvd = [] @user_unauthenticated.connect_via(app) async def un(myapp, **extra): assert request.path == "/profile" recvd.append("gotit") response = client.get("/profile", follow_redirects=False) assert len(recvd) == 1 assert response.location == "/login?next=/profile" def test_login_template_next(client): # Test that our login template propagates next. response = client.get("/profile", follow_redirects=True) assert "?next=/profile" in response.request.url login_url = get_form_action(response) response = client.post( login_url, data=dict(email="matt@lp.com", password="password"), follow_redirects=True, ) assert b"Profile Page" in response.data def test_authenticate_with_subdomain_next(app, client, get_message): app.config["SERVER_NAME"] = "lp.com" app.config["SECURITY_REDIRECT_ALLOW_SUBDOMAINS"] = True data = dict(email="matt@lp.com", password="password") response = client.post("/login?next=http://sub.lp.com", data=data) assert response.status_code in [302, 303] assert response.location == "http://sub.lp.com" @pytest.mark.settings(subdomain="auth") def test_authenticate_with_root_domain_next(app, client, get_message): # As of Flask 3.1 this must be explicitly set. app.subdomain_matching = True app.config["SERVER_NAME"] = "lp.com" app.config["SECURITY_REDIRECT_ALLOW_SUBDOMAINS"] = True data = dict(email="matt@lp.com", password="password") response = client.post("http://auth.lp.com/login?next=http://lp.com", data=data) assert response.status_code in [302, 303] assert response.location == "http://lp.com" def test_authenticate_with_invalid_subdomain_next(app, client, get_message): app.config["SERVER_NAME"] = "lp.com" app.config["SECURITY_REDIRECT_ALLOW_SUBDOMAINS"] = True data = dict(email="matt@lp.com", password="password") response = client.post("/login?next=http://sub.lp.net", data=data) assert get_message("INVALID_REDIRECT") in response.data def test_authenticate_with_subdomain_next_default_config(app, client, get_message): app.config["SERVER_NAME"] = "lp.com" data = dict(email="matt@lp.com", password="password") response = client.post("/login?next=http://sub.lp.com", data=data) assert get_message("INVALID_REDIRECT") in response.data @pytest.mark.settings( redirect_base_domain="bigidea.org", redirect_allowed_subdomains=["my.photo", "blog"] ) def test_allow_subdomains(app, client, get_message): app.config["SERVER_NAME"] = "app.bigidea.org" data = dict(email="matt@lp.com", password="password") # not in subdomain allowed list response = client.post("/login?next=http://blog2.bigidea.org", data=data) assert get_message("INVALID_REDIRECT") in response.data response = client.post("/login?next=http://my.photo.bigidea.org/image", data=data) assert response.location == "http://my.photo.bigidea.org/image" @pytest.mark.settings( redirect_base_domain="bigidea.org", redirect_allowed_subdomains=[] ) def test_redirect_allow_subdomains(app, client, get_message): app.config["SERVER_NAME"] = "bigidea.org" data = dict(email="matt@lp.com", password="password") response = client.post("/login?next=http://blog2.bigidea.org", data=data) assert get_message("INVALID_REDIRECT") in response.data response = client.post("/login?next=http://bigidea.org/imin", data=data) assert response.location == "http://bigidea.org/imin" @pytest.mark.settings( redirect_base_domain="myfrontend.org", redirect_allowed_subdomains=["."] ) def test_redirect_allow_base_domain(app, client, get_message): app.config["SERVER_NAME"] = "myflaskapp.org" data = dict(email="matt@lp.com", password="password") response = client.post("/login?next=http://nope.myfrontend.org", data=data) assert get_message("INVALID_REDIRECT") in response.data response = client.post("/login?next=http://myfrontend.org/imin", data=data) assert response.location == "http://myfrontend.org/imin" @pytest.mark.settings( redirect_base_domain="myfrontend.org", redirect_allowed_subdomains=[] ) def test_redirect_disallow_base_domain_without_dot(app, client, get_message): app.config["SERVER_NAME"] = "myflaskapp.org" data = dict(email="matt@lp.com", password="password") response = client.post("/login?next=http://myfrontend.org/imin", data=data) assert get_message("INVALID_REDIRECT") in response.data @pytest.mark.settings( redirect_base_domain="myapp.org:8080", redirect_allowed_subdomains=["."] ) def test_redirect_allow_other_ports(app, client, get_message): app.config["SERVER_NAME"] = "myapp.org" data = dict(email="matt@lp.com", password="password") response = client.post("/login?next=http://nope.myapp.org:8080", data=data) assert get_message("INVALID_REDIRECT") in response.data response = client.post("/login?next=http://myapp.org:8080/imin", data=data) assert response.location == "http://myapp.org:8080/imin" @pytest.mark.settings( post_login_view="http://blog.bigidea.org/post_login", redirect_base_domain="bigidea.org", redirect_allowed_subdomains=["my.photo", "blog"], ) def test_view_redirect(app, client, get_message): app.config["SERVER_NAME"] = "bigidea.org" data = dict(email="matt@lp.com", password="password") response = client.post("/login", data=data) assert response.location == "http://blog.bigidea.org/post_login" def test_authenticate_case_insensitive_email(app, client): response = authenticate(client, "MATT@lp.com", follow_redirects=True) assert b"Welcome matt@lp.com" in response.data def test_authenticate_with_invalid_input(client, get_message): response = client.post( "/login", json=dict(password="password", email="mememe@test.com"), headers={"Content-Type": "application/json"}, ) assert get_message("USER_DOES_NOT_EXIST") in response.data @pytest.mark.settings(post_login_view="/post_login") def test_get_already_authenticated(client): response = authenticate(client, follow_redirects=True) assert b"Welcome matt@lp.com" in response.data response = client.get("/login", follow_redirects=True) assert b"Post Login" in response.data # should still get extra goodies headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.get("/login", headers=headers) assert response.status_code == 200 jresponse = response.json["response"] assert all(a in jresponse for a in ["identity_attributes"]) assert "authentication_token" not in jresponse["user"] assert all(a in jresponse["user"] for a in ["email", "last_update"]) @pytest.mark.settings(post_login_view="/post_login") def test_get_already_authenticated_next(client): response = authenticate(client, follow_redirects=True) assert b"Welcome matt@lp.com" in response.data # This should NOT override post_login_view due to potential redirect loops. response = client.get("/login?next=/page1", follow_redirects=True) assert b"Post Login" in response.data @pytest.mark.settings(post_login_view="/post_login") def test_post_already_authenticated(client): response = authenticate(client, follow_redirects=True) assert b"Welcome matt@lp.com" in response.data data = dict(email="matt@lp.com", password="password") response = client.post("/login", data=data, follow_redirects=True) assert b"Post Login" in response.data # This should NOT override post_login_view due to potential redirect loops. response = client.post("/login?next=/page1", data=data, follow_redirects=True) assert b"Post Login" in response.data def test_login_form(client): response = client.post("/login", data={"email": "matt@lp.com"}) assert b"matt@lp.com" in response.data assert re.search(b']*type="email"[^>]*>', response.data) @pytest.mark.settings(username_enable=True) def test_login_form_username(client): # If USERNAME_ENABLE is set then login form should have a both an Email and # StringField response = client.get("/login") # Should be both email with Email type and username with autocomplete assert re.search(b']*type="email"[^>]*>', response.data) assert re.search(b']*autocomplete="username"[^>]*>', response.data) assert re.search(b']*autocomplete="current-password"[^>]*>', response.data) @pytest.mark.settings(username_enable=True, username_required=True) def test_login_form_username_required(app, client): # If username required - we should still be able to login with email alone # given default user_identity_attributes response = client.post( "/login", data=dict(email="matt@lp.com", password="password") ) assert check_location(app, response.location, "/") @pytest.mark.confirmable() @pytest.mark.settings( return_generic_responses=True, requires_confirmation_error_view="/confirm" ) def test_generic_response(app, client, get_message): response = client.post( "/login", data=dict(email="mattwho@lp.com", password="forgot") ) assert get_message("GENERIC_AUTHN_FAILED") in response.data response = client.post("/login", data=dict(email="matt@lp.com", password="forgot")) assert get_message("GENERIC_AUTHN_FAILED") in response.data response = client.post( "/login", json=dict(email="mattwho@lp.com", password="forgot") ) # make sure no field error key. assert list(response.json["response"]["field_errors"].keys()) == [""] assert len(response.json["response"]["field_errors"][""]) == 1 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "GENERIC_AUTHN_FAILED" ) response = client.post("/login", json=dict(email="matt@lp.com", password="forgot")) assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "GENERIC_AUTHN_FAILED" ) # make sure don't get confirmation required with capture_flashes() as flashes: response = client.post( "/login", data=dict(email="mattwho@lp.com", password="password"), follow_redirects=False, ) assert response.status_code == 200 assert len(flashes) == 0 @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.registerable() @pytest.mark.settings(username_enable=True, return_generic_responses=True) def test_generic_response_username(app, client, get_message): data = dict( email="dude@lp.com", username="dude", password="awesome sunset", password_confirm="awesome sunset", ) response = client.post("/register", json=data) assert response.headers["Content-Type"] == "application/json" assert response.status_code == 200 logout(client) response = client.post( "/login", data=dict( username="dude2", password="awesome sunset", password_confirm="awesome sunset", ), ) assert get_message("GENERIC_AUTHN_FAILED") in response.data response = client.post("/login", json=dict(username="dude2", password="forgot")) # make sure no field error key. assert list(response.json["response"]["field_errors"].keys()) == [""] assert len(response.json["response"]["field_errors"][""]) == 1 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "GENERIC_AUTHN_FAILED" ) def test_unprovided_username(client, get_message): response = authenticate(client, "") assert get_message("USER_DOES_NOT_EXIST") in response.data def test_unprovided_password(client, get_message): response = authenticate(client, password="") assert get_message("PASSWORD_NOT_PROVIDED") in response.data def test_invalid_user(client, get_message): response = authenticate(client, email="bogus@bogus.com") assert get_message("USER_DOES_NOT_EXIST") in response.data def test_bad_password(client, get_message, signals): response = authenticate(client, password="bogus") assert get_message("INVALID_PASSWORD") in response.data assert signals["user_failed_authn"][0]["request_endpoint"] == "security.login" assert signals["user_failed_authn"][0]["user_email"] == "matt@lp.com" assert signals["user_failed_authn"][0]["auth_type"] == "password" assert not signals["user_failed_authn"][0]["tfa"] def test_inactive_user(client, get_message): response = authenticate(client, "tiya@lp.com", "password") assert get_message("DISABLED_ACCOUNT") in response.data def test_inactive_forbids(app, client, get_message): """Make sure that existing session doesn't work after user marked inactive """ response = authenticate(client, follow_redirects=True) assert response.status_code == 200 # make sure can access restricted page response = client.get("/profile", follow_redirects=True) assert b"Profile Page" in response.data # deactivate matt with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") app.security.datastore.deactivate_user(user) app.security.datastore.commit() response = client.get("/profile", follow_redirects=False) print(response.data) # should be thrown back to login page. assert response.status_code in [302, 303] assert response.location == "/login?next=/profile" @pytest.mark.settings(unauthorized_view=None) def test_inactive_forbids_token(app, client_nc, get_message): """Make sure that existing token doesn't work after user marked inactive """ response = json_authenticate(client_nc) assert response.status_code == 200 token = response.json["response"]["user"]["authentication_token"] headers = {"Authentication-Token": token} # make sure can access restricted page response = client_nc.get("/token", headers=headers) assert b"Token Authentication" in response.data # deactivate matt with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") app.security.datastore.deactivate_user(user) app.security.datastore.commit() response = client_nc.get("/token", content_type="application/json", headers=headers) assert response.status_code == 401 def test_inactive_forbids_basic(app, client, get_message): """Make sure that basic auth doesn't work if user deactivated""" # Should properly work. response = client.get( "/multi_auth", headers={ "Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:password").decode("utf-8") }, ) assert b"Session, Token, Basic" in response.data # deactivate joe with app.test_request_context("/"): user = app.security.datastore.find_user(email="joe@lp.com") app.security.datastore.deactivate_user(user) app.security.datastore.commit() response = client.get( "/multi_auth", headers={ "Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:password").decode("utf-8") }, ) assert get_message("UNAUTHENTICATED")[0] in response.data def test_unset_password(client, get_message): response = authenticate(client, "jess@lp.com", "password") assert get_message("INVALID_PASSWORD") in response.data response = authenticate(client, "jess@lp.com", "") assert get_message("PASSWORD_NOT_PROVIDED") in response.data def _locked(self, form_error): if self.email == "gal@lp.com": form_error.append("You are not allowed to do that") return True return False @pytest.mark.app_settings(TESTING_USER_INJECT=dict(is_locked=_locked)) def test_override_user_locked(app, client, get_message): data = dict(email="jill@lp.com", password="password") response = client.post("/login", json=data) assert response.status_code == 200 logout(client) data = dict(email="gal@lp.com", password="password") response = client.post("/login", json=data) assert response.status_code == 400 assert response.json["response"]["errors"] == ["You are not allowed to do that"] assert response.json["response"]["field_errors"]["email"] == [ "You are not allowed to do that" ] @pytest.mark.app_settings(TESTING_USER_INJECT=dict(is_locked=_locked)) @pytest.mark.settings(return_generic_responses=True) def test_override_user_locked_gr(app, client, get_message): data = dict(email="gal@lp.com", password="password") response = client.post("/login", json=data) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "GENERIC_AUTHN_FAILED" ) assert "email" not in response.json["response"]["field_errors"] def test_logout(client): authenticate(client) response = logout(client, follow_redirects=True) assert b"Home Page" in response.data def test_logout_post(client): authenticate(client) response = client.post("/logout", content_type="application/json") assert response.status_code == 200 assert response.json["meta"]["code"] == 200 def test_logout_with_next_invalid(client, get_message): authenticate(client) response = client.post("/logout?next=http://google.com") assert "google.com" not in response.location def test_logout_with_next(client): authenticate(client) response = client.post("/logout?next=/page1", follow_redirects=True) assert b"Page 1" in response.data def test_missing_session_access(client, get_message): response = client.get("/profile", follow_redirects=True) assert get_message("UNAUTHENTICATED") in response.data def test_has_session_access(client): authenticate(client) response = client.get("/profile", follow_redirects=True) assert b"profile" in response.data def test_authorized_access(client): authenticate(client) response = client.get("/admin") assert b"Admin Page" in response.data def test_unauthorized_access(client, get_message): authenticate(client, "joe@lp.com") response = client.get("/admin", follow_redirects=True) assert response.status_code == 403 def test_unauthorized_callable_view(app, sqlalchemy_datastore, get_message): # Test various options using custom unauthorized view def unauthz_view(): from flask import request if request.path == "/admin": return None elif request.path == "/admin_perm": return "" elif request.path == "/admin_and_editor": return "/profile" elif request.path == "/simple": # N.B. security issue - app should verify this is local return request.referrer else: return "not_implemented" app.config["SECURITY_UNAUTHORIZED_VIEW"] = unauthz_view init_app_with_options(app, sqlalchemy_datastore) client = app.test_client() # activate tiya with app.test_request_context("/"): user = app.security.datastore.find_user(email="tiya@lp.com") app.security.datastore.activate_user(user) app.security.datastore.commit() authenticate(client, "tiya@lp.com") assert is_authenticated(client, get_message) response = client.get("/admin") assert response.status_code == 403 response = client.get("/admin_perm") assert response.status_code == 403 response = client.get("/admin_and_editor", follow_redirects=False) assert check_location(app, response.location, "/profile") response = client.get(response.location) assert response.data.count(get_message("UNAUTHORIZED")) == 1 response = client.get( "/simple", headers={"referer": "/myhome"}, follow_redirects=False ) assert check_location(app, response.location, "/myhome") def test_unauthorized_url_view(app, sqlalchemy_datastore): # Test unknown endpoint basically results in redirect to the given string. app.config["SECURITY_UNAUTHORIZED_VIEW"] = ".myendpoint" init_app_with_options(app, sqlalchemy_datastore) client = app.test_client() authenticate(client, "tiya@lp.com") response = client.get("/admin") assert response.status_code in [302, 303] check_location(app, response.location, ".myendpoint") @pytest.mark.settings(unauthorized_view="/unauthz") def test_roles_accepted(clients): # This specifically tests that we can pass a URL for unauthorized_view. for user in ("matt@lp.com", "joe@lp.com"): authenticate(clients, user) response = clients.get("/admin_or_editor") assert b"Admin or Editor Page" in response.data logout(clients) authenticate(clients, "jill@lp.com") response = clients.get("/admin_or_editor", follow_redirects=True) assert b"Unauthorized" in response.data @pytest.mark.settings(unauthorized_view="unauthz") def test_permissions_accepted(clients): for user in ("matt@lp.com", "joe@lp.com"): authenticate(clients, user) response = clients.get("/admin_perm") assert b"Admin Page with full-write or super" in response.data logout(clients) authenticate(clients, "jill@lp.com") response = clients.get("/admin_perm", follow_redirects=True) assert b"Unauthorized" in response.data @pytest.mark.settings(unauthorized_view="unauthz") def test_permissions_required(clients): for user in ["matt@lp.com"]: authenticate(clients, user) response = clients.get("/admin_perm_required") assert b"Admin Page required" in response.data logout(clients) authenticate(clients, "joe@lp.com") response = clients.get("/admin_perm_required", follow_redirects=True) assert b"Unauthorized" in response.data @pytest.mark.settings(unauthorized_view="unauthz") def test_unauthenticated_role_required(client, get_message): response = client.get("/admin", follow_redirects=True) assert get_message("UNAUTHORIZED") in response.data @pytest.mark.settings(unauthorized_view="unauthz") def test_multiple_role_required(clients): for user in ("matt@lp.com", "joe@lp.com"): authenticate(clients, user) response = clients.get("/admin_and_editor", follow_redirects=True) assert b"Unauthorized" in response.data clients.post("/logout") authenticate(clients, "dave@lp.com") response = clients.get("/admin_and_editor", follow_redirects=True) assert b"Admin and Editor Page" in response.data def test_ok_json_auth(client): response = json_authenticate(client) assert response.json["meta"]["code"] == 200 assert "authentication_token" in response.json["response"]["user"] def test_invalid_json_auth(client): response = json_authenticate(client, password="junk") assert b'"code": 400' in response.data def test_isstring_non_string_email(client): response = client.post( "/login", json=dict(email={"not": "a string"}, password="password") ) assert response.status_code == 400 def test_token_auth_via_querystring_valid_token(client): response = json_authenticate(client) token = response.json["response"]["user"]["authentication_token"] response = client.get("/token?auth_token=" + token) assert b"Token Authentication" in response.data def test_token_auth_via_header_valid_token(client): response = json_authenticate(client) token = response.json["response"]["user"]["authentication_token"] headers = {"Authentication-Token": token} response = client.get("/token", headers=headers) assert b"Token Authentication" in response.data def test_token_auth_via_querystring_invalid_token(client): response = client.get("/token?auth_token=X", headers={"Accept": "application/json"}) assert response.status_code == 401 def test_token_auth_via_header_invalid_token(client): response = client.get( "/token", headers={"Authentication-Token": "X", "Accept": "application/json"} ) assert response.status_code == 401 def test_token_auth_invalid_for_session_auth(client): # when user is loaded from token data, session authentication should fail. response = json_authenticate(client) token = response.json["response"]["user"]["authentication_token"] # logout so session doesn't contain valid user details logout(client) headers = {"Authentication-Token": token, "Accept": "application/json"} response = client.get("/session", headers=headers) assert response.status_code == 401 def test_garbled_auth_token(app, client_nc): # garble token def augment_auth_token(tdata): del tdata["exp"] app.config["TESTING_AUGMENT_AUTH_TOKEN"] = augment_auth_token response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] verify_token(client_nc, token, status=401) @pytest.mark.csrf(ignore_unauth=True, csrfprotect=True) def test_token_auth_csrf(client): response = json_authenticate(client) token = response.json["response"]["user"]["authentication_token"] csrf_token = response.json["response"]["csrf_token"] headers = {"Authentication-Token": token} response = client.post("/token", headers=headers) assert b"The CSRF token is missing" in response.data # test JSON version response = client.post("/token", headers=headers, content_type="application/json") assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." # now do it right headers["X-CSRF-Token"] = csrf_token response = client.post( "/token", headers=headers, ) assert b"Token Authentication" in response.data def test_http_auth(client, get_message): # browsers expect 401 response with WWW-Authenticate header - which will prompt # them to pop up a login form. response = client.get("/http", headers={}) assert response.status_code == 401 assert get_message("UNAUTHENTICATED") in response.data assert "WWW-Authenticate" in response.headers assert 'Basic realm="Login Required"' == response.headers["WWW-Authenticate"] # Now provide correct credentials response = client.get( "/http", headers={ "Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:password").decode("utf-8") }, ) assert b"HTTP Authentication" in response.data @pytest.mark.settings( USER_IDENTITY_ATTRIBUTES=[ {"email": {"mapper": uia_email_mapper}}, {"username": {"mapper": lambda x: x}}, ] ) def test_http_auth_username(client): response = client.get( "/http", headers={ "Authorization": "Basic %s" % base64.b64encode(b"jill:password").decode("utf-8") }, ) assert b"HTTP Authentication" in response.data def test_http_auth_no_authorization(client): response = client.get( "/http_admin_required", headers={ "Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:password").decode("utf-8") }, ) assert response.status_code == 403 def test_http_auth_no_authorization_json(client, get_message): response = client.get( "/http_admin_required", headers={ "accept": "application/json", "Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:password").decode("utf-8"), }, ) assert response.status_code == 403 assert response.headers["Content-Type"] == "application/json" def test_http_auth_no_authentication(client, get_message): response = client.get("/http", headers={}) assert response.status_code == 401 assert get_message("UNAUTHENTICATED") in response.data assert "WWW-Authenticate" in response.headers assert 'Basic realm="Login Required"' == response.headers["WWW-Authenticate"] def test_http_auth_no_authentication_json(client, get_message): response = client.get("/http", headers={"accept": "application/json"}) assert response.status_code == 401 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "UNAUTHENTICATED" ) assert response.headers["Content-Type"] == "application/json" def test_invalid_http_auth_invalid_username(client, get_message): response = client.get( "/http", headers={ "Authorization": "Basic %s" % base64.b64encode(b"bogus:bogus").decode("utf-8") }, ) assert get_message("UNAUTHENTICATED") in response.data assert "WWW-Authenticate" in response.headers assert 'Basic realm="Login Required"' == response.headers["WWW-Authenticate"] def test_invalid_http_auth_invalid_username_json(client, get_message): # Even with JSON - Basic Auth required a WWW-Authenticate header response. response = client.get( "/http", headers={ "accept": "application/json", "Authorization": "Basic %s" % base64.b64encode(b"bogus:bogus").decode("utf-8"), }, ) assert response.status_code == 401 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "UNAUTHENTICATED" ) assert response.headers["Content-Type"] == "application/json" assert "WWW-Authenticate" in response.headers def test_invalid_http_auth_bad_password(client, get_message): response = client.get( "/http", headers={ "Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:bogus").decode("utf-8") }, ) assert get_message("UNAUTHENTICATED") in response.data assert "WWW-Authenticate" in response.headers assert 'Basic realm="Login Required"' == response.headers["WWW-Authenticate"] def test_custom_http_auth_realm(client, get_message): response = client.get( "/http_custom_realm", headers={ "Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:bogus").decode("utf-8") }, ) assert get_message("UNAUTHENTICATED") in response.data assert "WWW-Authenticate" in response.headers assert 'Basic realm="My Realm"' == response.headers["WWW-Authenticate"] @pytest.mark.csrf(csrfprotect=True) def test_http_auth_csrf(client, get_message): headers = { "Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:password").decode("utf-8") } response = client.post( "/http", headers=headers, ) assert b"The CSRF token is missing" in response.data # test JSON version response = client.post("/http", headers=headers, content_type="application/json") assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." # grab a csrf_token response = client.get("/login") csrf_token = get_form_input_value(response, "csrf_token") headers["X-CSRF-Token"] = csrf_token response = client.post( "/http", headers=headers, ) assert b"HTTP Authentication" in response.data def test_multi_auth_basic(client): response = client.get( "/multi_auth", headers={ "Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:password").decode("utf-8") }, ) assert b"Basic" in response.data response = client.get("/multi_auth") # Default unauthn with basic is to return 401 with WWW-Authenticate Header # so that browser pops up a username/password dialog assert response.status_code == 401 assert "WWW-Authenticate" in response.headers def test_multi_auth_basic_invalid(client, get_message): response = client.get( "/multi_auth", headers={ "Authorization": "Basic %s" % base64.b64encode(b"bogus:bogus").decode("utf-8") }, ) assert get_message("UNAUTHENTICATED") in response.data assert "WWW-Authenticate" in response.headers assert 'Basic realm="Login Required"' == response.headers["WWW-Authenticate"] response = client.get("/multi_auth") assert response.status_code == 401 def test_multi_auth_token(client): response = json_authenticate(client) token = response.json["response"]["user"]["authentication_token"] response = client.get("/multi_auth?auth_token=" + token) assert b"Token" in response.data def test_multi_auth_session(client): authenticate(client) response = client.get("/multi_auth") assert b"Session" in response.data def test_authenticated_loop(client): # If user is already authenticated say via session, and then hits an endpoint # protected with @auth_token_required() - then they will be redirected to the login # page which will simply note the current user is already logged in and redirect # to POST_LOGIN_VIEW. Between 3.3.0 and 3.4.4 - this redirect would honor the 'next' # parameter - thus redirecting back to the endpoint that caused the redirect in the # first place - thus an infinite loop. authenticate(client) response = client.get("/token", follow_redirects=True) assert response.status_code == 200 assert b"Home Page" in response.data def test_user_deleted_during_session_reverts_to_anonymous_user(app, client): authenticate(client) with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") app.security.datastore.delete_user(user) app.security.datastore.commit() response = client.get("/") assert b"Hello matt@lp.com" not in response.data def test_session_loads_identity(app, client): @app.route("/identity_check") @auth_required("session") def id_check(): if hasattr(g, "identity"): identity = g.identity assert hasattr(identity, "loader_called") assert identity.loader_called return "Success" json_authenticate(client) # add identity loader after authentication to only fire it for # session-authentication next `get` call @identity_loaded.connect_via(app) def identity_loaded_check(sender, identity): identity.loader_called = True response = client.get("/identity_check") assert b"Success" == response.data def test_remember_token(client): response = authenticate(client, follow_redirects=False, remember=True) client.delete_cookie("session") assert not client.get_cookie("session") assert client.get_cookie("remember_token") response = client.get("/profile", follow_redirects=True) assert b"Profile Page" in response.data def test_remember_fresh(client): # authenticating via remember cookie shouldn't be considered fresh response = authenticate(client, follow_redirects=False, remember=True) client.delete_cookie("session") assert not client.get_cookie("session") assert client.get_cookie("remember_token") # this should magically log us in response = client.get("/profile", follow_redirects=True) assert b"Profile Page" in response.data sess = get_existing_session(client) assert sess response = client.get("/fresh", headers={"Content-Type": "application/json"}) assert response.json["response"]["reauth_required"] def test_request_loader_does_not_fail_with_invalid_token(client): client.set_cookie("remember_token") response = client.get("/") assert b"BadSignature" not in response.data def test_sending_auth_token_with_json(client): response = json_authenticate(client) token = response.json["response"]["user"]["authentication_token"] data = '{"auth_token": "%s"}' % token response = client.post( "/token", data=data, headers={"Content-Type": "application/json"} ) assert b"Token Authentication" in response.data def test_json_not_dict(client): response = client.post( "/json", data=json.dumps(["thing1", "thing2"]), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 def test_login_info(client): # Make sure we can get user info when logged in already. json_authenticate(client) response = client.get("/login", headers={"Content-Type": "application/json"}) assert response.status_code == 200 assert response.cache_control.private assert response.json["response"]["user"]["email"] == "matt@lp.com" assert "last_update" in response.json["response"]["user"] response = client.get("/login", headers={"Accept": "application/json"}) assert response.status_code == 200 assert response.json["response"]["user"]["email"] == "matt@lp.com" assert "last_update" in response.json["response"]["user"] @pytest.mark.registerable() @pytest.mark.settings(post_login_view="/anon_required") def test_anon_required(client, get_message): """If logged in, should get 'anonymous_user_required' redirect""" response = authenticate(client, follow_redirects=False) response = client.get("/register") assert "location" in response.headers assert "/anon_required" in response.location @pytest.mark.registerable() @pytest.mark.settings(post_login_view="/anon_required") def test_anon_required_json(client, get_message): """If logged in, should get 'anonymous_user_required' response""" authenticate(client, follow_redirects=False) response = client.get("/register", headers={"Accept": "application/json"}) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "ANONYMOUS_USER_REQUIRED" ) def test_change_uniquifier(app, client_nc): # make sure that existing token no longer works once we change the uniquifier response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] verify_token(client_nc, token) # now change uniquifier with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") app.security.datastore.reset_user_access(user) app.security.datastore.commit() verify_token(client_nc, token, status=401) # get new token and verify it works response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] verify_token(client_nc, token) def test_verifying_token_from_version_3x(app, client): """ Check token generated with flask security 3.x, which has different form than token from version 4.0.0, can be verified """ from .test_utils import get_auth_token_version_3x with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") token = get_auth_token_version_3x(app, user) headers = {"Authentication-Token": token, "Accept": "application/json"} response = client.get("/profile", headers=headers) assert response.status_code == 200 def test_verifying_token_from_version_4x(app, client): from .test_utils import get_auth_token_version_4x with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") token = get_auth_token_version_4x(app, user) headers = {"Authentication-Token": token, "Accept": "application/json"} response = client.get("/profile", headers=headers) assert response.status_code == 200 def test_change_token_uniquifier(app): pytest.importorskip("sqlalchemy") pytest.importorskip("flask_sqlalchemy") # make sure that existing token no longer works once we change the token uniquifier from sqlalchemy import Column, String from flask_sqlalchemy import SQLAlchemy from flask_security.models import fsqla_v2 as fsqla from flask_security import Security, SQLAlchemyUserDatastore app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" db = SQLAlchemy(app) fsqla.FsModels.set_db_info(db) class Role(db.Model, fsqla.FsRoleMixin): pass class User(db.Model, fsqla.FsUserMixin): fs_token_uniquifier = Column(String(64), unique=True, nullable=False) with app.app_context(): db.create_all() ds = SQLAlchemyUserDatastore(db, User, Role) app.security = Security(app, datastore=ds) with app.app_context(): ds.create_user( email="matt@lp.com", password=hash_password("password"), ) ds.commit() client_nc = app.test_client(use_cookies=False) response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] verify_token(client_nc, token) # now change uniquifier with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") app.security.datastore.reset_user_access(user) app.security.datastore.commit() verify_token(client_nc, token, status=401) # get new token and verify it works response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] verify_token(client_nc, token) with app.app_context(): db.engine.dispose() def test_null_token_uniquifier(app): pytest.importorskip("sqlalchemy") pytest.importorskip("flask_sqlalchemy") # If existing record has a null fs_token_uniquifier, should be set on first use. from sqlalchemy import Column, String from flask_sqlalchemy import SQLAlchemy from flask_security.models import fsqla_v2 as fsqla from flask_security import Security, SQLAlchemyUserDatastore app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" db = SQLAlchemy(app) fsqla.FsModels.set_db_info(db) class Role(db.Model, fsqla.FsRoleMixin): pass class User(db.Model, fsqla.FsUserMixin): fs_token_uniquifier = Column(String(64), unique=True, nullable=True) with app.app_context(): db.create_all() ds = SQLAlchemyUserDatastore(db, User, Role) app.security = Security(app, datastore=ds) with app.app_context(): ds.create_user( email="matt@lp.com", password=hash_password("password"), ) ds.commit() # manually null out fs_token_uniquifier user = ds.find_user(email="matt@lp.com") user.fs_token_uniquifier = None ds.put(user) ds.commit() client_nc = app.test_client(use_cookies=False) response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] verify_token(client_nc, token) with app.app_context(): db.engine.dispose() def test_token_query(app, client_nc): # Verify that when authenticating with auth token (and not session) # that there is just one DB query to get user. with capture_queries(app.security.datastore) as queries: response = json_authenticate(client_nc) assert len(queries) == 1 token = response.json["response"]["user"]["authentication_token"] response = client_nc.get( "/token", headers={"Content-Type": "application/json", "Authentication-Token": token}, ) assert response.status_code == 200 assert len(queries) == 2 def test_session_query(in_app_context): # Verify that when authenticating with auth token (but also sending session) # that there are 2 DB queries to get user. # This is since the session will load one - but auth_token_required needs to # verify that the TOKEN is valid (and it is possible that the user_id in the # session is different that the one in the token (huh?) myapp = in_app_context populate_data(myapp) myclient = myapp.test_client() response = json_authenticate(myclient) token = response.json["response"]["user"]["authentication_token"] with capture_queries(myapp.security.datastore) as queries: response = myclient.get( "/token", headers={"Content-Type": "application/json", "Authentication-Token": token}, ) assert response.status_code == 200 assert len(queries) == 2 @pytest.mark.changeable() def test_no_get_auth_token(app, client): # Test that GETs don't return an auth token. This is a security issue since # GETs aren't protected with CSRF authenticate(client) response = client.get( "/login?include_auth_token", headers={"Content-Type": "application/json"} ) assert "authentication_token" not in response.json["response"]["user"] data = dict( password="password", new_password="new strong password", new_password_confirm="new strong password", ) response = client.get( "/change?include_auth_token", json=data, headers={"Content-Type": "application/json"}, ) assert "authentication_token" not in response.json["response"]["user"] def test_auth_token_decorator(app, client_nc): """ Test accessing endpoint decorated with auth_token_required when using token generated by flask security 3.x algorithm """ with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") token = get_auth_token_version_3x(app, user) response = client_nc.get( "/token", headers={"Content-Type": "application/json", "Authentication-Token": token}, ) assert response.status_code == 200 @pytest.mark.settings(cache_control={"no-store": True}) def test_override_cache_control(app, client_nc): response = json_authenticate(client_nc) assert not response.cache_control.private assert response.cache_control.no_store @pytest.mark.settings(cache_control=None) def test_no_cache_control(app, client_nc): response = json_authenticate(client_nc) assert "Cache-Control" not in response.headers def test_cache_control_not_on_app_endpoints(app, client): # SECURITY_CACHE_CONTROL applies to Flask-Security endpoints only - # responses from application endpoints must not be touched. response = client.get("/login") assert response.cache_control.no_store response = client.get("/") assert "Cache-Control" not in response.headers pallets-eco-flask-security-72bf929/tests/test_change_email.py000066400000000000000000000225051525721743500244210ustar00rootroot00000000000000""" test_change_email ~~~~~~~~~~~~~~~~~ Change email functionality tests :copyright: (c) 2024-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from contextlib import contextmanager from datetime import date, timedelta import re from urllib.parse import urlsplit import pytest from freezegun import freeze_time from tests.test_utils import ( authenticate, capture_flashes, is_authenticated, json_authenticate, logout, ) from flask_security import hash_password from flask_security.signals import ( change_email_instructions_sent, change_email_confirmed, ) pytestmark = pytest.mark.change_email() @contextmanager def capture_change_email_requests(): change_email_requests = [] def _on(app, **data): change_email_requests.append(data) change_email_instructions_sent.connect(_on) try: yield change_email_requests finally: change_email_instructions_sent.disconnect(_on) @pytest.mark.settings( change_email_error_view="/change-email", change_email_within=timedelta(seconds=67) ) def test_ce(app, clients, get_message, outbox): @change_email_confirmed.connect_via(app) def _on(app, **kwargs): assert kwargs["old_email"] == "matt@lp.com" assert kwargs["user"].email == "matt2@lp.com" authenticate(clients, email="matt@lp.com") with capture_change_email_requests() as ce_requests: response = clients.post("/change-email", data={"email": ""}) assert get_message("INVALID_EMAIL_ADDRESS") in response.data assert len(outbox) == 0 response = clients.post("/change-email", data=dict(email="matt2@lp.com")) msg = get_message("CHANGE_EMAIL_SENT", email="matt2@lp.com") assert msg in response.data assert "matt2@lp.com" == ce_requests[0]["new_email"] token = ce_requests[0]["token"] assert len(outbox) == 1 assert "1 minute and 7 seconds" in outbox[0].body response = clients.get("/change-email/" + token, follow_redirects=True) assert get_message("CHANGE_EMAIL_CONFIRMED") in response.data assert is_authenticated(clients, get_message) logout(clients) authenticate(clients, email="matt2@lp.com") assert is_authenticated(clients, get_message) # try using link again - should fail with capture_flashes() as flashes: clients.get("/change-email/" + token, follow_redirects=True) assert flashes[0]["message"].encode("utf-8") == get_message("API_ERROR") def test_ce_json(app, client, get_message, outbox): headers = {"Accept": "application/json", "Content-Type": "application/json"} @change_email_confirmed.connect_via(app) def _on(app, **kwargs): assert kwargs["old_email"] == "matt@lp.com" assert kwargs["user"].email == "matt2@lp.com" json_authenticate(client, email="matt@lp.com") with capture_change_email_requests() as ce_requests: response = client.post("/change-email", json={"email": ""}) assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_EMAIL_ADDRESS" ) assert len(outbox) == 0 response = client.post("/change-email", json=dict(email="matt2@lp.com")) assert response.status_code == 200 assert response.json["response"]["current_email"] == "matt@lp.com" assert "matt2@lp.com" == ce_requests[0]["new_email"] token = ce_requests[0]["token"] assert len(outbox) == 1 response = client.get( "/change-email/" + token, headers=headers, follow_redirects=True ) assert get_message("CHANGE_EMAIL_CONFIRMED") in response.data assert is_authenticated(client, get_message) logout(client) authenticate(client, email="matt2@lp.com") assert is_authenticated(client, get_message) @pytest.mark.settings( change_email_within=timedelta(minutes=36), change_email_error_view="/change-email" ) def test_expired_token(client, get_message): # Note that we need relatively new-ish date since session cookies also expire. with freeze_time(date.today() + timedelta(days=-1)): authenticate(client, email="matt@lp.com") with capture_change_email_requests() as ce_requests: client.post("/change-email", data=dict(email="matt2@lp.com")) assert "matt2@lp.com" == ce_requests[0]["new_email"] token = ce_requests[0]["token"] response = client.get("/change-email/" + token, follow_redirects=True) msg = get_message("CHANGE_EMAIL_EXPIRED", within="36 minutes") assert msg in response.data @pytest.mark.settings( change_email_within=timedelta(minutes=90), change_email_email_template="security/email/change_email_instructions_test", email_html=False, ) def test_template(app, client, get_message, outbox): # Check contents of email template - this uses a test template # to check all context vars since the default template # doesn't have all of them. authenticate(client, email="matt@lp.com") with capture_change_email_requests() as ce_requests: client.post("/change-email", data=dict(email="matt2@lp.com")) # check email assert outbox[0].recipients[0] == "matt2@lp.com" matcher = re.findall(r"\w+:.*", outbox[0].body, re.IGNORECASE) # should be 5 - link, email, token, config item, within assert matcher[1].split(":")[1] == "matt@lp.com" assert matcher[2].split(":")[1] == ce_requests[0]["token"] assert matcher[3].split(":")[1] == "True" # register_blueprint assert matcher[4].split(":")[1] == "1 hour and 30 minutes" # within # entire original template is included in this test one assert "This link will expire in 1 hour and 30 minutes." in outbox[0].body # check link _, link = matcher[0].split(":", 1) response = client.get(link, follow_redirects=True) assert get_message("CHANGE_EMAIL_CONFIRMED") in response.data @pytest.mark.settings( change_email_email_template="security/email/change_email_instructions_test", email_html=False, ) @pytest.mark.parametrize("humanizer", ["fr_FR"], indirect=["humanizer"]) def test_template_fr(app, client, get_message, outbox, humanizer): # Check contents of email template - with humanize xlation of within authenticate(client, email="matt@lp.com") with capture_change_email_requests(): client.post("/change-email", data=dict(email="matt2@lp.com")) # check email assert outbox[0].recipients[0] == "matt2@lp.com" matcher = re.findall(r"\w+:.*", outbox[0].body, re.IGNORECASE) assert matcher[4].split(":")[1] == "2 heures" @pytest.mark.settings(return_generic_responses=True) def test_generic_response(app, client, get_message, outbox): authenticate(client, email="matt@lp.com") with capture_change_email_requests(): # first try bad formatted email - should get detailed error response = client.post("/change-email", json={"email": ""}) assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_EMAIL_ADDRESS" ) # try existing email - should get same response as if it 'worked' response = client.post("/change-email", data=dict(email="gal@lp.com")) msg = get_message("CHANGE_EMAIL_SENT", email="gal@lp.com") assert msg in response.data # but no email was actually sent assert len(outbox) == 0 @pytest.mark.settings( redirect_host="myui.com:8090", redirect_behavior="spa", post_change_email_view="/change-email-redirect", change_email_error_view="/change-email-error", ) def test_spa_get(app, client, get_message): json_authenticate(client, email="matt@lp.com") with capture_change_email_requests() as ce_requests: response = client.post("/change-email", data=dict(email="matt2@lp.com")) msg = get_message("CHANGE_EMAIL_SENT", email="matt2@lp.com") assert msg in response.data assert "matt2@lp.com" == ce_requests[0]["new_email"] token = ce_requests[0]["token"] response = client.get("/change-email/" + token, follow_redirects=False) assert response.status_code in [302, 303] split = urlsplit(response.headers["Location"]) assert "myui.com:8090" == split.netloc assert "/change-email-redirect" == split.path # again - should be an error response = client.get("/change-email/" + token, follow_redirects=False) assert response.status_code in [302, 303] split = urlsplit(response.headers["Location"]) assert "myui.com:8090" == split.netloc assert "/change-email-error" == split.path @pytest.mark.settings(change_email_error_view="/change-email") def test_ce_race(app, client, get_message): # test that if an email is taken between the link being sent and # the user confirming - they get an error authenticate(client, email="matt@lp.com") with capture_change_email_requests() as ce_requests: client.post("/change-email", data=dict(email="matt2@lp.com")) token = ce_requests[0]["token"] with app.app_context(): app.security.datastore.create_user( email="matt2@lp.com", password=hash_password("password"), ) app.security.datastore.commit() with capture_flashes() as flashes: client.get("/change-email/" + token, follow_redirects=True) assert flashes[0]["message"].encode("utf-8") == get_message("API_ERROR") pallets-eco-flask-security-72bf929/tests/test_change_username.py000066400000000000000000000265211525721743500251530ustar00rootroot00000000000000""" test_change_username ~~~~~~~~~~~~~~~~~~~~ Change username tests :copyright: (c) 2025-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import pytest from flask import Flask from flask_security import UsernameUtil, UserMixin, username_changed from flask_security.forms import _default_field_labels from flask_security.utils import localize_callback from tests.test_utils import ( authenticate, check_location, get_form_input_value, init_app_with_options, is_authenticated, logout, ) pytestmark = pytest.mark.change_username() @pytest.mark.settings( post_change_username_view="/post_change_username", username_enable=True ) def test_cu(app, clients, get_message, outbox): recorded = [] @username_changed.connect_via(app) def on_username_changed(app, user, old_username): assert isinstance(app, Flask) assert isinstance(user, UserMixin) recorded.append((user, old_username)) authenticate(clients) # Test change view response = clients.get("/change-username", follow_redirects=True) assert b"Change Username" in response.data # test length validation response = clients.post( "/change-username", data={"username": "me"}, follow_redirects=True, ) assert get_message("USERNAME_INVALID_LENGTH", min=4, max=32) in response.data # Test successful submit sends email notification response = clients.post( "/change-username", data={"username": "memynewusername"}, follow_redirects=True, ) assert get_message("USERNAME_CHANGE") in response.data assert b"Post Change Username" in response.data assert len(recorded) == 1 assert len(outbox) == 1 assert "Your username has been changed" in outbox[0].body response = clients.get("/change-username", follow_redirects=True) assert b"memynewusername" in response.data # authenticate with new username logout(clients) clients.post("/login", data=dict(username="memynewusername", password="password")) assert is_authenticated(clients, get_message) # Test same as previous response = clients.post( "/change-username", data={"username": "memynewusername"}, follow_redirects=True, ) assert ( get_message("USERNAME_ALREADY_ASSOCIATED", username="memynewusername") in response.data ) # since username isn't required - change it to nothing response = clients.post( "/change-username", data={"username": ""}, follow_redirects=True, ) assert get_message("USERNAME_CHANGE") in response.data assert b"Post Change Username" in response.data assert len(recorded) == 2 assert len(outbox) == 2 assert "Your username has been changed" in outbox[1].body # shouldn't be able to log in with username logout(clients) clients.post("/login", data=dict(username="memynewusername", password="password")) assert not is_authenticated(clients, get_message) @pytest.mark.settings( post_change_username_view="/post_change_username", username_enable=True ) def test_cu_json(app, clients, get_message): # Test JSON recorded = [] @username_changed.connect_via(app) def on_username_changed(app, user, old_username): recorded.append((user, old_username)) response = clients.get("/change-username", content_type="application/json") assert response.status_code == 401 authenticate(clients) response = clients.get("/change-username", content_type="application/json") assert response.json["response"]["current_username"] == "matt" response = clients.post("/change-username", json={"username": "memyjsonusername"}) assert response.status_code == 200 assert response.headers["Content-Type"] == "application/json" assert len(recorded) == 1 user, old = recorded[0] assert old == "matt" # Test JSON errors response = clients.post("/change-username", json={"username": "my"}) assert response.status_code == 400 assert response.json["response"]["field_errors"]["username"] == [ "Username must be at least 4 characters and less than 32 characters" ] # authenticate with old username logout(clients) clients.post("/login", json=dict(username="matt", password="password")) assert not is_authenticated(clients, get_message) # authenticate with new username logout(clients) clients.post("/login", json=dict(username="memyjsonusername", password="password")) assert is_authenticated(clients, get_message) @pytest.mark.settings(username_enable=True, username_required=True) def test_cu_required(app, client, get_message): client.post("/login", json=dict(username="matt", password="password")) response = client.post("/change-username", json={"username": ""}) assert ( get_message("USERNAME_NOT_PROVIDED") == response.json["response"]["field_errors"]["username"][0].encode() ) @pytest.mark.babel(babel_default_locale="cic_US", test_xlations=True) def test_xlation(app, client, get_message_local, outbox): # Test form and email translation authenticate(client) response = client.get("/change-username", follow_redirects=True) with app.test_request_context(): # Check header header = localize_callback("Change Username") assert "OKAY!" in header # ensures actual translation assert f"

{header}

".encode() in response.data submit = localize_callback(_default_field_labels["submit"]) assert "OKAY!" in submit assert f'value="{submit}"'.encode() in response.data response = client.post( "/change-username", data={ "username": "mynewusername", }, follow_redirects=True, ) with app.test_request_context(): flash = get_message_local("USERNAME_CHANGE") assert "OKAY!" in flash assert f'
  • {flash}
  • '.encode() in response.data assert b"Home Page" in response.data assert len(outbox) == 1 subj = localize_callback( app.config["SECURITY_EMAIL_SUBJECT_USERNAME_CHANGE_NOTICE"] ) assert "OKAY!" in subj assert subj in outbox[0].subject body = localize_callback("Your username has been changed.") assert "OKAY!" in body # assert str(markupsafe.escape(body)) in outbox[0].alts["html"] assert body in outbox[0].body @pytest.mark.settings(change_username_url="/custom-change-username") def test_custom_change_url(client): authenticate(client) response = client.get("/custom-change-username") assert response.status_code == 200 assert b"Change Username" in response.data @pytest.mark.settings(change_username_template="custom_security/change_username.html") def test_custom_change_template(client): authenticate(client) response = client.get("/change-username") assert b"CUSTOM CHANGE USERNAME" in response.data @pytest.mark.settings(send_username_change_email=False) def test_disable_change_emails(app, client, outbox): authenticate(client) response = client.post( "/change-username", json={"username": "mynewusername"}, follow_redirects=True, ) assert response.status_code == 200 assert len(outbox) == 0 @pytest.mark.settings(post_change_username_view="/profile") def test_custom_post_change_view(client): authenticate(client) response = client.post( "/change-username", data={"username": "mynewusername"}, follow_redirects=True, ) assert b"Profile Page" in response.data def test_my_validator(app, sqlalchemy_datastore): class MyUsernameUtil(UsernameUtil): def validate(self, username): if username == "nowayjose": return "Are you crazy?", None return username, None init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"username_util_cls": MyUsernameUtil}}, ) tcl = app.test_client() authenticate(tcl) response = tcl.post("/change-username", json=dict(username="nowayjose")) assert response.status_code == 400 assert "Are you crazy" in response.json["response"]["errors"][0] @pytest.mark.settings(username_enable=True) def test_username_normalize(app, client): """Verify that can log in with both original and normalized username""" authenticate(client) response = client.post( "/change-username", json=dict(username="newusername\N{ROMAN NUMERAL ONE}") ) assert response.status_code == 200 logout(client) # use original typed-in username response = client.post( "/login", json=dict(username="newusername\N{ROMAN NUMERAL ONE}", password="password"), ) assert response.status_code == 200 logout(client) # try with normalized username response = client.post( "/login", json=dict( username="newusername\N{LATIN CAPITAL LETTER I}", password="password", ), ) assert response.status_code == 200 @pytest.mark.settings(input_normalize_form=None, username_enable=True) def test_username_no_normalize(app, client): """Verify that can log in with original but not normalized if have disabled normalization """ authenticate(client) response = client.post( "/change-username", json=dict(username="newusername\N{ROMAN NUMERAL ONE}") ) assert response.status_code == 200 logout(client) # try with normalized password - should fail response = client.post( "/login", json=dict( username="newusername\N{LATIN CAPITAL LETTER I}", password="password" ), ) assert response.status_code == 400 # use original typed-in username response = client.post( "/login", json=dict(username="newusername\N{ROMAN NUMERAL ONE}", password="password"), ) assert response.status_code == 200 @pytest.mark.csrf(ignore_unauth=True) @pytest.mark.settings(post_change_username_view="/post_change_username_view") def test_csrf(app, client): # enable CSRF, make sure template shows CSRF errors. authenticate(client) data = { "username": "mynewusername", } response = client.post("/change-username", data=data) assert b"The CSRF token is missing" in response.data # Note that we get a CSRF token EVEN for errors - this seems odd # but can't find anything that says its a security issue csrf_token = get_form_input_value(response, "csrf_token") data["csrf_token"] = csrf_token response = client.post("/change-username", data=data) assert check_location(app, response.location, "/post_change_username_view") @pytest.mark.csrf(ignore_unauth=True, csrfprotect=True) def test_csrf_json(app, client): # This tests the handle_csrf code path - especially the JSON code path # that should return a JSON response! authenticate(client) response = client.post("/change-username", json=dict(username="mynewusername")) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." response = client.get("/change-username", content_type="application/json") csrf_token = response.json["response"]["csrf_token"] response = client.post( "/change-username", json=dict(username="mynewusername"), headers={"X-CSRF-Token": csrf_token}, ) assert response.status_code == 200 pallets-eco-flask-security-72bf929/tests/test_changeable.py000066400000000000000000000546301525721743500241020ustar00rootroot00000000000000""" test_changeable ~~~~~~~~~~~~~~~ Changeable tests :copyright: (c) 2019-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import base64 import pytest from flask import Flask import markupsafe from flask_security import PasswordUtil, UserMixin, password_changed, user_authenticated from flask_security.forms import _default_field_labels from flask_security.utils import localize_callback from tests.test_utils import ( authenticate, check_location, get_form_input_value, get_session, hash_password, init_app_with_options, json_authenticate, logout, ) pytestmark = pytest.mark.changeable() def test_changeable_flag(app, clients, get_message, outbox): recorded = [] @password_changed.connect_via(app) def on_password_changed(app, user): assert isinstance(app, Flask) assert isinstance(user, UserMixin) recorded.append(user) authenticate(clients) # Test change view response = clients.get("/change", follow_redirects=True) assert b"Change Password" in response.data # Test wrong original password response = clients.post( "/change", data={ "password": "notpassword", "new_password": "newpassword", "new_password_confirm": "newpassword", }, follow_redirects=True, ) assert get_message("INVALID_PASSWORD") in response.data # Test mismatch response = clients.post( "/change", data={ "password": "password", "new_password": "newpassword", "new_password_confirm": "notnewpassword", }, follow_redirects=True, ) assert get_message("RETYPE_PASSWORD_MISMATCH") in response.data # Test missing password response = clients.post( "/change", data={"password": " ", "new_password": "", "new_password_confirm": ""}, follow_redirects=True, ) assert get_message("PASSWORD_NOT_PROVIDED") in response.data response = clients.post( "/change", data={ "password": " ", "new_password": "awesome password", "new_password_confirm": "awesome password", }, follow_redirects=True, ) assert get_message("PASSWORD_NOT_PROVIDED") in response.data # Test bad password response = clients.post( "/change", data={"password": "password", "new_password": "a", "new_password_confirm": "a"}, follow_redirects=True, ) assert get_message("PASSWORD_INVALID_LENGTH", length=8) in response.data # Test same as previous response = clients.post( "/change", data={ "password": "password", "new_password": "password", "new_password_confirm": "password", }, follow_redirects=True, ) assert get_message("PASSWORD_IS_THE_SAME") in response.data # Test successful submit sends email notification response = clients.post( "/change", data={ "password": "password", "new_password": "new strong password", "new_password_confirm": "new strong password", }, follow_redirects=True, ) assert get_message("PASSWORD_CHANGE") in response.data assert b"Home Page" in response.data assert len(recorded) == 1 assert len(outbox) == 1 assert "Your password has been changed" in outbox[0].body # Test leading and trailing whitespace not stripped response = clients.post( "/change", data={ "password": "new strong password", "new_password": " new strong password ", "new_password_confirm": " new strong password ", }, follow_redirects=True, ) assert get_message("PASSWORD_CHANGE") in response.data # Test JSON data = ( '{"password": " new strong password ", ' '"new_password": "new stronger password2", ' '"new_password_confirm": "new stronger password2"}' ) response = clients.post( "/change", data=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 200 assert response.headers["Content-Type"] == "application/json" # Test JSON errors data = '{"password": "newpassword"}' response = clients.post( "/change", data=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert response.json["response"]["field_errors"]["new_password"] == [ "Password not provided" ] def test_change_invalidates_session(app, client): # Make sure that if we change our password - prior sessions are invalidated. # changing password effectively re-logs in user - verify the signal auths = [] @user_authenticated.connect_via(app) def authned(myapp, user, **extra_args): auths.append((user.email, extra_args["authn_via"])) # No remember cookie since that also be reset and auto-login. data = dict(email="matt@lp.com", password="password", remember="") response = client.post("/login", data=data) sess = get_session(response) cur_user_id = sess.get("_user_id", sess.get("user_id")) response = client.post( "/change", data={ "password": "password", "new_password": "new strong password", "new_password_confirm": "new strong password", }, follow_redirects=True, ) # First auth was the initial login above - second should be from /change assert auths[1][0] == "matt@lp.com" assert "change" in auths[1][1] # Should have received a new session cookie - so should still be logged in response = client.get("/profile", follow_redirects=True) assert b"Profile Page" in response.data # Now use old session - shouldn't work. with client.session_transaction() as oldsess: oldsess["_user_id"] = cur_user_id oldsess["user_id"] = cur_user_id # try to access protected endpoint - shouldn't work response = client.get("/profile") assert response.status_code in [302, 303] assert response.location == "/login?next=/profile" def test_change_updates_remember(app, client): # Test that on change password - remember cookie updated authenticate(client, remember=True) response = client.post( "/change", data={ "password": "password", "new_password": "new strong password", "new_password_confirm": "new strong password", }, follow_redirects=True, ) # Should have received a new session cookie - so should still be logged in response = client.get("/profile", follow_redirects=True) assert b"Profile Page" in response.data assert client.get_cookie("remember_token") client.delete_cookie("session") response = client.get("/profile", follow_redirects=True) assert b"Profile Page" in response.data def test_change_invalidates_auth_token(app, client): # if change password, by default that should invalidate auth tokens response = json_authenticate(client) token = response.json["response"]["user"]["authentication_token"] headers = {"Authentication-Token": token} # make sure can access restricted page response = client.get("/token", headers=headers) assert b"Token Authentication" in response.data response = client.post( "/change", data={ "password": "password", "new_password": "new strong password", "new_password_confirm": "new strong password", }, follow_redirects=True, ) assert response.status_code == 200 # authtoken should now be invalid response = client.get("/token", headers=headers) assert response.status_code in [302, 303] assert response.location == "/login?next=/token" def test_auth_uniquifier(app): pytest.importorskip("sqlalchemy") pytest.importorskip("flask_sqlalchemy") # If add fs_token_uniquifier to user model - change password shouldn't invalidate # auth tokens. from sqlalchemy import Column, String from flask_sqlalchemy import SQLAlchemy from flask_security.models import fsqla_v2 as fsqla from flask_security import Security, SQLAlchemyUserDatastore app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" db = SQLAlchemy(app) fsqla.FsModels.set_db_info(db) class Role(db.Model, fsqla.FsRoleMixin): pass class User(db.Model, fsqla.FsUserMixin): fs_token_uniquifier = Column(String(64), unique=True, nullable=False) with app.app_context(): db.create_all() ds = SQLAlchemyUserDatastore(db, User, Role) app.security = Security(app, datastore=ds) with app.app_context(): ds.create_user( email="matt@lp.com", password=hash_password("password"), ) ds.commit() tcl = app.test_client() # standard login with auth token response = json_authenticate(tcl) token = response.json["response"]["user"]["authentication_token"] headers = {"Authentication-Token": token} # make sure can access restricted page response = tcl.get("/token", headers=headers) assert b"Token Authentication" in response.data # change password response = tcl.post( "/change", data={ "password": "password", "new_password": "new strong password", "new_password_confirm": "new strong password", }, follow_redirects=True, ) assert response.status_code == 200 # authtoken should still be valid response = tcl.get("/token", headers=headers) assert response.status_code == 200 with app.app_context(): db.engine.dispose() @pytest.mark.babel(babel_default_locale="cic_US", test_xlations=True) def test_xlation(app, client, get_message_local, outbox): # Test form and email translation authenticate(client) response = client.get("/change", follow_redirects=True) with app.test_request_context(): # Check header header = localize_callback("Change Password") assert "OKAY!" in header # ensures actual translation assert f"

    {header}

    ".encode() in response.data submit = localize_callback(_default_field_labels["change_password"]) assert "OKAY!" in submit assert f'value="{submit}"'.encode() in response.data response = client.post( "/change", data={ "password": "password", "new_password": "new strong password", "new_password_confirm": "new strong password", }, follow_redirects=True, ) with app.test_request_context(): flash = get_message_local("PASSWORD_CHANGE") assert "OKAY!" in flash assert f'
  • {flash}
  • '.encode() in response.data assert b"Home Page" in response.data assert len(outbox) == 1 subj = localize_callback( app.config["SECURITY_EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE"] ) assert "OKAY!" in subj assert subj in outbox[0].subject body = localize_callback("Your password has been changed.") assert "OKAY!" in body assert str(markupsafe.escape(body)) in outbox[0].alts["html"] assert body in outbox[0].body @pytest.mark.settings(change_url="/custom_change") def test_custom_change_url(client): authenticate(client) response = client.get("/custom_change") assert response.status_code == 200 @pytest.mark.settings(change_password_template="custom_security/change_password.html") def test_custom_change_template(client): authenticate(client) response = client.get("/change") assert b"CUSTOM CHANGE PASSWORD" in response.data @pytest.mark.settings(send_password_change_email=False) def test_disable_change_emails(app, client, outbox): client.post( "/change", data={ "password": "password", "new_password": "newpassword", "new_password_confirm": "newpassword", }, follow_redirects=True, ) assert len(outbox) == 0 @pytest.mark.settings(post_change_view="/profile") def test_custom_post_change_view(client): authenticate(client) response = client.post( "/change", data={ "password": "password", "new_password": "new strong password", "new_password_confirm": "new strong password", }, follow_redirects=True, ) assert b"Profile Page" in response.data def test_token_change(app, client_nc): # Verify can change password using token auth only login_response = json_authenticate(client_nc) token = login_response.json["response"]["user"]["authentication_token"] data = dict( password="password", new_password="new strong password", new_password_confirm="new strong password", ) response = client_nc.post( "/change?include_auth_token=1", json=data, headers={"Content-Type": "application/json", "Authentication-Token": token}, ) assert response.status_code == 200 assert "authentication_token" in response.json["response"]["user"] @pytest.mark.settings(api_enabled_methods=["basic"]) def test_basic_change(app, client_nc, get_message): # Verify can change password using basic auth data = dict( password="password", new_password="new strong password", new_password_confirm="new strong password", ) response = client_nc.post("/change", data=data) assert get_message("UNAUTHENTICATED") in response.data assert "WWW-Authenticate" in response.headers response = client_nc.post( "/change", data=data, headers={ "Authorization": "Basic %s" % base64.b64encode(b"matt@lp.com:password").decode("utf-8") }, follow_redirects=True, ) assert response.status_code == 200 # No session so no flashing assert b"Home Page" in response.data def __test_easy_password(client): authenticate(client) data = ( '{"password": "password", ' '"new_password": "mattmatt2", ' '"new_password_confirm": "mattmatt2"}' ) response = client.post( "/change", data=data, headers={"Content-Type": "application/json"} ) assert response.headers["Content-Type"] == "application/json" return response @pytest.mark.settings(password_complexity_checker="zxcvbn") def test_easy_password(app, client): response = __test_easy_password(client) assert response.status_code == 400 # Response from zxcvbn assert "Repeats like" in response.json["response"]["errors"][0] @pytest.mark.settings(password_complexity_checker="zxcvbn", zxcvbn_minimum_score=0) def test_easy_password_ok(app, client): response = __test_easy_password(client) assert response.status_code == 200 def test_my_validator(app, sqlalchemy_datastore): class MyPwUtil(PasswordUtil): def validate(self, password, is_register, **kwargs): user = kwargs["user"] # This is setup in createusers for matt. assert user.security_number == 123456 return ["Are you crazy?"], password init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"password_util_cls": MyPwUtil}} ) tcl = app.test_client() authenticate(tcl) data = ( '{"password": "password", ' '"new_password": "mattmatt2", ' '"new_password_confirm": "mattmatt2"}' ) response = tcl.post( "/change", data=data, headers={"Content-Type": "application/json"} ) assert response.headers["Content-Type"] == "application/json" assert response.status_code == 400 assert "Are you crazy" in response.json["response"]["errors"][0] @pytest.mark.settings(password_length_min=12) def test_override_length(app, client, get_message): authenticate(client) response = client.post( "/change", data={ "password": "password", "new_password": "01234567890", "new_password_confirm": "01234567890", }, follow_redirects=True, ) assert get_message("PASSWORD_INVALID_LENGTH", length=12) in response.data def test_unicode_length(app, client, get_message): # From NIST and OWASP - each unicode code point should count as a character. authenticate(client) # Emoji's are 4 bytes in utf-8 data = dict( password="password", new_password="\N{CYCLONE}\N{SUNRISE}\N{FOGGY}" "\N{VOLCANO}\N{CRESCENT MOON}\N{MILKY WAY}" "\N{FOG}\N{THERMOMETER}\N{ROSE}", new_password_confirm="\N{CYCLONE}\N{SUNRISE}\N{FOGGY}" "\N{VOLCANO}\N{CRESCENT MOON}\N{MILKY WAY}" "\N{FOG}\N{THERMOMETER}\N{ROSE}", ) response = client.post( "/change", json=data, headers={"Content-Type": "application/json"} ) assert response.headers["Content-Type"] == "application/json" assert response.status_code == 200 def test_unicode_invalid_length(app, client, get_message): # From NIST and OWASP - each unicode code point should count as a character. authenticate(client) # Emoji's are 4 bytes in utf-8 data = dict( password="password", new_password="\N{CYCLONE}\N{CYCLONE}\N{FOGGY}\N{FOGGY}", new_password_confirm="\N{CYCLONE}\N{CYCLONE}\N{FOGGY}\N{FOGGY}", ) response = client.post( "/change", json=data, headers={"Content-Type": "application/json"} ) assert response.headers["Content-Type"] == "application/json" assert response.status_code == 400 assert get_message("PASSWORD_INVALID_LENGTH", length=8) in response.data def test_pwd_normalize(app, client): """Verify that can log in with both original and normalized pwd""" authenticate(client) data = dict( password="password", new_password="new strong password\N{ROMAN NUMERAL ONE}", new_password_confirm="new strong password\N{ROMAN NUMERAL ONE}", ) response = client.post( "/change", json=data, headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 logout(client) # use original typed-in pwd response = client.post( "/login", json=dict( email="matt@lp.com", password="new strong password\N{ROMAN NUMERAL ONE}" ), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 logout(client) # try with normalized password response = client.post( "/login", json=dict( email="matt@lp.com", password="new strong password\N{LATIN CAPITAL LETTER I}", ), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 # Verify can change password using original password data = dict( password="new strong password\N{ROMAN NUMERAL ONE}", new_password="new strong password\N{ROMAN NUMERAL TWO}", new_password_confirm="new strong password\N{ROMAN NUMERAL TWO}", ) response = client.post( "/change", json=data, headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 @pytest.mark.settings(password_normalize_form=None) def test_pwd_no_normalize(app, client): """Verify that can log in with original but not normalized if have disabled normalization """ authenticate(client) data = dict( password="password", new_password="new strong password\N{ROMAN NUMERAL ONE}", new_password_confirm="new strong password\N{ROMAN NUMERAL ONE}", ) response = client.post( "/change", json=data, headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 logout(client) # try with normalized password - should fail response = client.post( "/login", json=dict( email="matt@lp.com", password="new strong password\N{LATIN CAPITAL LETTER I}", ), headers={"Content-Type": "application/json"}, ) assert response.status_code == 400 # use original typed-in pwd response = client.post( "/login", json=dict( email="matt@lp.com", password="new strong password\N{ROMAN NUMERAL ONE}" ), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 # Verify can change password using original password data = dict( password="new strong password\N{ROMAN NUMERAL ONE}", new_password="new strong password\N{ROMAN NUMERAL TWO}", new_password_confirm="new strong password\N{ROMAN NUMERAL TWO}", ) response = client.post( "/change", json=data, headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 @pytest.mark.csrf(ignore_unauth=True) @pytest.mark.settings(post_change_view="/post_change_view") def test_csrf(app, client): # enable CSRF, make sure template shows CSRF errors. authenticate(client) data = { "password": "password", "new_password": "new strong password", "new_password_confirm": "new strong password", } response = client.post("/change", data=data) assert b"The CSRF token is missing" in response.data # Note that we get a CSRF token EVEN for errors - this seems odd # but can't find anything that says its a security issue csrf_token = get_form_input_value(response, "csrf_token") data["csrf_token"] = csrf_token response = client.post("/change", data=data) assert check_location(app, response.location, "/post_change_view") @pytest.mark.csrf(ignore_unauth=True, csrfprotect=True) def test_csrf_json(app, client): # This tests the handle_csrf code path - especially the JSON code path # that should return a JSON response! authenticate(client) data = { "password": "password", "new_password": "new strong password", "new_password_confirm": "new strong password", } response = client.post("/change", json=data) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." # check form path also response = client.post("/change", data=data) assert response.status_code == 400 assert b"The CSRF token is missing." in response.data response = client.get("/change", content_type="application/json") csrf_token = response.json["response"]["csrf_token"] response = client.post("/change", json=data, headers={"X-CSRF-Token": csrf_token}) assert response.status_code == 200 pallets-eco-flask-security-72bf929/tests/test_cli.py000066400000000000000000000315231525721743500225740ustar00rootroot00000000000000""" test_cli ~~~~~~~~ Test command line interface. """ from click.testing import CliRunner import pytest from flask_security.cli import ( roles_add, roles_create, roles_remove, roles_add_permissions, roles_remove_permissions, users_activate, users_change_password, users_create, users_deactivate, users_reset_access, ) from flask_security import verify_password, PeeweeUserDatastore def test_cli_createuser(script_info): """Test create user CLI.""" runner = CliRunner() # Missing params result = runner.invoke(users_create, input="1234\n1234\n", obj=script_info) assert result.exit_code != 0 # Create user with invalid email result = runner.invoke( users_create, ["not-an-email", "--password", "battery staple"], obj=script_info ) assert result.exit_code == 2 # Create user result = runner.invoke( users_create, ["email@example.tld", "--password", "battery staple"], obj=script_info, ) assert result.exit_code == 0 # create user with email and phone number result = runner.invoke( users_create, [ "email1@example.tld", "us_phone_number:5551212", "--password", "battery staple", ], obj=script_info, ) assert result.exit_code == 0 # try to activate using phone number result = runner.invoke(users_activate, "5551212", obj=script_info) assert result.exit_code == 0 def test_cli_createuser_extraargs(script_info): # Test that passing attributes that aren't part of registration form # are passed to create_user runner = CliRunner() result = runner.invoke( users_create, [ "email1@example.tld", "security_number:666", "--password", "battery staple", "--active", ], obj=script_info, ) assert result.exit_code == 0 result = runner.invoke(users_activate, ["email1@example.tld"], obj=script_info) assert result.exit_code == 0 assert "was already activated" in result.output app = script_info.load_app() with app.app_context(): user = app.security.datastore.find_user(email="email1@example.tld") assert user.security_number == 666 def test_cli_createuser_unknown_extraargs(script_info): # Test that passing attributes that aren't part of registration form # and aren't part of user model raise an error runner = CliRunner() result = runner.invoke( users_create, [ "email1@example.tld", "mysecretnumber:666", "--password", "battery staple", "--active", ], obj=script_info, ) # peewee doesn't throw errors for unknown attributes if not isinstance(script_info._loaded_app.security.datastore, PeeweeUserDatastore): assert result.exit_code == 2 # different datastores raise different errors/exceptions assert "Error: Invalid value:" in result.output assert "'mysecretnumber'" in result.output def test_cli_createuser_normalize(script_info): """Test create user CLI that it properly normalizes email and password.""" runner = CliRunner() result = runner.invoke( users_create, ["email@example.tld", "--password", "battery staple\N{ROMAN NUMERAL ONE}"], obj=script_info, ) assert result.exit_code == 0 assert "email@example.tld" in result.stdout app = script_info.load_app() with app.app_context(): user = app.security.datastore.find_user(email="email@example.tld") assert verify_password( "battery staple\N{LATIN CAPITAL LETTER I}", user.password ) def test_cli_createuser_errors(script_info): # check that errors are stringified runner = CliRunner() result = runner.invoke( users_create, ["--password", "battery staple"], obj=script_info ) assert result.exit_code == 2 assert "Email not provided" in result.output @pytest.mark.babel(babel_default_locale="cic_US", test_xlations=True) def test_cli_locale(script_info): runner = CliRunner() result = runner.invoke( users_create, ["--password", "battery staple"], obj=script_info ) assert result.exit_code == 2 assert "OKAY!" in result.output def test_cli_createrole(script_info): """Test create user CLI.""" runner = CliRunner() # Missing params result = runner.invoke(roles_create, ["-d", "Test description"], obj=script_info) assert result.exit_code != 0 # Create role result = runner.invoke( roles_create, ["superusers", "-d", "Test description"], obj=script_info ) assert result.exit_code == 0 def test_cli_createrole_with_perms(script_info): """Test create user CLI.""" runner = CliRunner() # Create role result = runner.invoke( roles_create, ["superusers", "-d", "Test description", "-p", "super, full-write"], obj=script_info, ) # Some datastores don't support permissions. assert result.exit_code == 0 or result.exit_code == 2 def test_cli_addremove_role(script_info): """Test add/remove role.""" runner = CliRunner() # Create a user and a role result = runner.invoke( users_create, ["a@example.tld", "--password", "battery staple"], obj=script_info ) assert result.exit_code == 0 result = runner.invoke(roles_create, ["superuser"], obj=script_info) assert result.exit_code == 0 # User not found result = runner.invoke( roles_add, ["inval@example.tld", "superuser"], obj=script_info ) assert result.exit_code != 0 # Add: result = runner.invoke(roles_add, ["a@example.tld", "invalid"], obj=script_info) assert result.exit_code != 0 result = runner.invoke( roles_remove, ["inval@example.tld", "superuser"], obj=script_info ) assert result.exit_code != 0 # Remove: result = runner.invoke(roles_remove, ["a@example.tld", "invalid"], obj=script_info) assert result.exit_code != 0 result = runner.invoke( roles_remove, ["b@example.tld", "superuser"], obj=script_info ) assert result.exit_code != 0 result = runner.invoke( roles_remove, ["a@example.tld", "superuser"], obj=script_info ) assert result.exit_code != 0 # Add: result = runner.invoke(roles_add, ["a@example.tld", "superuser"], obj=script_info) assert result.exit_code == 0 result = runner.invoke(roles_add, ["a@example.tld", "superuser"], obj=script_info) assert result.exit_code != 0 # Remove: result = runner.invoke( roles_remove, ["a@example.tld", "superuser"], obj=script_info ) assert result.exit_code == 0 def test_cli_addremove_permissions(script_info): """Test add/remove permissions.""" runner = CliRunner() app = script_info.load_app() result = runner.invoke( roles_create, ["superusers", "-d", "Test description"], obj=script_info ) assert result.exit_code == 0 # add permission to non-existent role result = runner.invoke( roles_add_permissions, ["whatrole", "read, write"], obj=script_info ) assert "Cannot find role" in result.output result = runner.invoke( roles_add_permissions, ["superusers", "read, write"], obj=script_info ) with app.app_context(): srole = app.security.datastore.find_role("superusers") assert srole.get_permissions() == {"read", "write"} assert all(p in result.output for p in ["read", "write", "superusers"]) # remove permission to non-existent role result = runner.invoke( roles_remove_permissions, ["whatrole", "read, write"], obj=script_info ) assert "Cannot find role" in result.output result = runner.invoke( roles_remove_permissions, ["superusers", "write"], obj=script_info ) assert all(p in result.output for p in ["write", "superusers"]) result = runner.invoke( roles_remove_permissions, ["superusers", "whatever, read"], obj=script_info ) # remove permissions doesn't check if existing or not. assert all(p in result.output for p in ["read", "superusers"]) with app.app_context(): srole = app.security.datastore.find_role("superusers") assert srole.get_permissions() == set() def test_cli_activate_deactivate(script_info): """Test create user CLI.""" runner = CliRunner() # Create a user result = runner.invoke( users_create, ["a@example.tld", "--password", "battery staple"], obj=script_info ) assert result.exit_code == 0 # Activate result = runner.invoke(users_activate, ["in@valid.org"], obj=script_info) assert result.exit_code != 0 result = runner.invoke(users_deactivate, ["in@valid.org"], obj=script_info) assert result.exit_code != 0 result = runner.invoke(users_activate, ["a@example.tld"], obj=script_info) assert result.exit_code == 0 result = runner.invoke(users_activate, ["a@example.tld"], obj=script_info) assert result.exit_code == 0 # Deactivate result = runner.invoke(users_deactivate, ["a@example.tld"], obj=script_info) assert result.exit_code == 0 result = runner.invoke(users_deactivate, ["a@example.tld"], obj=script_info) assert result.exit_code == 0 def test_cli_reset_user(script_info): runner = CliRunner() result = runner.invoke( users_create, [ "email1@example.tld", "us_phone_number:5551212", "--password", "battery staple", ], obj=script_info, ) result = runner.invoke(users_reset_access, ["5551212"], obj=script_info) assert result.exit_code == 0 result = runner.invoke(users_reset_access, ["5551212"], obj=script_info) assert result.exit_code == 2 result = runner.invoke(users_reset_access, ["4441212"], obj=script_info) assert "User not found" in result.output def test_cli_change_password(script_info): runner = CliRunner() runner.invoke( users_create, ["email1@example.tld", "--password", "battery staple"], obj=script_info, ) result = runner.invoke( users_change_password, ["email1@example.tld", "--password", "battery_staple"], obj=script_info, ) assert result.exit_code == 0 # check too short a password result = runner.invoke( users_change_password, ["email1@example.tld", "--password", "hi"], obj=script_info, ) assert result.exit_code == 2 assert "Password must be at least" in result.output # check that password is properly normalized result = runner.invoke( users_change_password, ["email1@example.tld", "--password", "battery staple\N{ROMAN NUMERAL ONE}"], obj=script_info, ) assert result.exit_code == 0 app = script_info.load_app() with app.app_context(): user = app.security.datastore.find_user(email="email1@example.tld") assert verify_password( "battery staple\N{LATIN CAPITAL LETTER I}", user.password ) result = runner.invoke(users_change_password, ["--help"]) assert "IDENTITY" in result.output # check unknown user result = runner.invoke( users_change_password, ["wrong_email@example.tld", "--password", "battery_staple"], obj=script_info, ) assert result.exit_code == 2 assert "User not found" in result.output @pytest.mark.settings(use_register_v2=False) def test_cli_createuser_old(script_info): """Test create user CLI.""" runner = CliRunner() # Create user result = runner.invoke( users_create, ["email@example.tld", "--password", "battery staple"], obj=script_info, ) assert result.exit_code == 0 @pytest.mark.settings(use_register_v2=False) def test_cli_createuserold_attr(script_info): """Test create user CLI passing attr that is in User but not in form.""" runner = CliRunner() # Create user result = runner.invoke( users_create, [ "email@example.tld", "--password", "battery staple", "us_phone_number:5551212", "--username", "iamuser", ], obj=script_info, ) assert result.exit_code == 0 app = script_info.load_app() with app.app_context(): user = app.security.datastore.find_user(email="email@example.tld") assert user.us_phone_number == "5551212" user = app.security.datastore.find_user(username="iamuser") assert user def test_cli_create_nousername(script_info_min): """Test create user CLI passing attr that is in User but not in form.""" runner = CliRunner() # Create user result = runner.invoke( users_create, [ "email@example.tld", "--password", "battery staple", ], obj=script_info_min, ) assert result.exit_code == 0 pallets-eco-flask-security-72bf929/tests/test_configuration.py000066400000000000000000000025301525721743500246700ustar00rootroot00000000000000""" test_configuration ~~~~~~~~~~~~~~~~~~ Basic configuration tests """ import base64 import pytest from tests.test_utils import authenticate, logout @pytest.mark.settings( logout_url="/custom_logout", login_url="/custom_login", post_login_view="/post_login", post_logout_view="/post_logout", default_http_auth_realm="Custom Realm", ) def test_view_configuration(client, get_message): response = client.get("/custom_login") assert b"

    Login

    " in response.data response = authenticate(client, endpoint="/custom_login") assert "location" in response.headers assert "/post_login" in response.location response = logout(client, endpoint="/custom_logout") assert "location" in response.headers assert "/post_logout" in response.location response = client.get( "/http", headers={"Authorization": "Basic %s" % base64.b64encode(b"joe@lp.com:bogus")}, ) assert response.status_code == 401 assert get_message("UNAUTHENTICATED") in response.data assert "WWW-Authenticate" in response.headers assert 'Basic realm="Custom Realm"' == response.headers["WWW-Authenticate"] @pytest.mark.settings(login_user_template="custom_security/login_user.html") def test_template_configuration(client): response = client.get("/login") assert b"CUSTOM LOGIN USER" in response.data pallets-eco-flask-security-72bf929/tests/test_confirmable.py000066400000000000000000000677461525721743500243260ustar00rootroot00000000000000""" test_confirmable ~~~~~~~~~~~~~~~~ Confirmable tests """ from datetime import date, timedelta import re from urllib.parse import parse_qsl, urlsplit import pytest from flask import Flask from freezegun import freeze_time from wtforms.fields import StringField from wtforms.validators import Length from flask_security.core import Security, UserMixin from flask_security.signals import confirm_instructions_sent, user_confirmed from flask_security.forms import SendConfirmationForm from tests.test_utils import ( authenticate, capture_flashes, capture_registrations, check_location, is_authenticated, logout, populate_data, ) pytestmark = pytest.mark.confirmable() @pytest.mark.registerable() def test_confirmable_flag(app, clients, get_message): recorded_confirms = [] recorded_instructions_sent = [] @user_confirmed.connect_via(app) def on_confirmed(app, user): assert isinstance(app, Flask) assert isinstance(user, UserMixin) recorded_confirms.append(user) @confirm_instructions_sent.connect_via(app) def on_instructions_sent(app, **kwargs): assert isinstance(app, Flask) assert isinstance(kwargs["user"], UserMixin) assert isinstance(kwargs["token"], str) assert isinstance(kwargs["confirmation_token"], str) recorded_instructions_sent.append(kwargs["user"]) # Test login before confirmation email = "dude@lp.com" with capture_registrations() as registrations: data = dict( email=email, password="awesome sunset", password_confirm="awesome sunset", next="", ) response = clients.post("/register", data=data) assert response.status_code in [302, 303] response = authenticate(clients, email=email, password="awesome sunset") assert get_message("CONFIRMATION_REQUIRED") in response.data # Test invalid token response = clients.get("/confirm/bogus", follow_redirects=True) assert get_message("INVALID_CONFIRMATION_TOKEN") in response.data # Test JSON response = clients.post( "/confirm", json=dict(email="matt@lp.com"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 assert response.headers["Content-Type"] == "application/json" assert "user" not in response.json["response"] assert len(recorded_instructions_sent) == 1 # Test ask for instructions with invalid email response = clients.post("/confirm", data=dict(email="bogus@bogus.com")) assert get_message("USER_DOES_NOT_EXIST") in response.data # Test resend instructions response = clients.post("/confirm", data=dict(email=email)) assert get_message("CONFIRMATION_REQUEST", email=email) in response.data assert len(recorded_instructions_sent) == 2 # Test confirm token = registrations[0]["confirm_token"] response = clients.get("/confirm/" + token, follow_redirects=False) assert len(recorded_confirms) == 1 response = clients.get(response.location) assert get_message("EMAIL_CONFIRMED") in response.data # make sure not logged in assert not is_authenticated(clients, get_message) # Test already confirmed response = clients.get("/confirm/" + token, follow_redirects=True) assert get_message("ALREADY_CONFIRMED") in response.data assert len(recorded_instructions_sent) == 2 # Test already confirmed when asking for confirmation instructions response = clients.get("/confirm") assert response.status_code == 200 response = clients.post("/confirm", data=dict(email=email)) assert get_message("ALREADY_CONFIRMED") in response.data # Test if user was deleted before confirmation with capture_registrations() as registrations: data = dict( email="mary27@lp.com", password="awesome sunset", password_confirm="awesome sunset", next="", ) clients.post("/register", data=data) user = registrations[0]["user"] token = registrations[0]["confirm_token"] with app.app_context(): app.security.datastore.delete(user) app.security.datastore.commit() if hasattr(app.security.datastore.db, "close_db") and callable( app.security.datastore.db.close_db ): app.security.datastore.db.close_db(None) response = clients.get("/confirm/" + token, follow_redirects=True) assert get_message("INVALID_CONFIRMATION_TOKEN") in response.data @pytest.mark.registerable() @pytest.mark.settings( email_template_confirm="security/email/confirmation_instructions_test", email_html=False, ) def test_confirmation_template(app, client, get_message, outbox): # Check contents of email template - this uses a test template # in order to check all context vars since the default template # doesn't have all of them. recorded_tokens_sent = [] @confirm_instructions_sent.connect_via(app) def on_instructions_sent(app, **kwargs): recorded_tokens_sent.append(kwargs["confirmation_token"]) with capture_registrations() as registrations: data = dict( email="mary@lp.com", password="awesome sunset", password_confirm="awesome sunset", next="", ) # Register - this will use the welcome template client.post("/register", data=data, follow_redirects=True) assert len(outbox) == 1 # check registration email matcher = re.findall(r"\w+:.*", outbox[0].body, re.IGNORECASE) # should be 4 - link, email, token, config item assert matcher[1].split(":")[1] == "mary@lp.com" assert matcher[2].split(":")[1] == registrations[0]["confirm_token"] assert matcher[2].split(":")[1] == registrations[0]["confirmation_token"] assert matcher[3].split(":")[1] == "True" # register_blueprint # Explicitly ask for confirmation - # this will use the confirmation_instructions template client.post("/confirm", data=dict(email="mary@lp.com")) # check confirmation email matcher = re.findall(r"\w+:.*", outbox[1].body, re.IGNORECASE) # should be 5 - link, email, token, config item, within assert matcher[1].split(":")[1] == "mary@lp.com" assert matcher[2].split(":")[1] == recorded_tokens_sent[0] token = matcher[2].split(":")[1] assert token == recorded_tokens_sent[0] assert matcher[3].split(":")[1] == "True" # register_blueprint assert matcher[4].split(":")[1] == "2 days" # within # check link _, link = matcher[0].split(":", 1) response = client.get(link, follow_redirects=True) assert get_message("EMAIL_CONFIRMED") in response.data # regular text template is included in the test template assert "This link will expire in 2 days." in outbox[1].body @pytest.mark.registerable() @pytest.mark.settings(requires_confirmation_error_view="/confirm") def test_requires_confirmation_error_redirect(app, clients): data = dict( email="jyl@lp.com", password="awesome sunset", password_confirm="awesome sunset" ) response = clients.post("/register", data=data) response = authenticate( clients, email="jyl@lp.com", password="awesome sunset", follow_redirects=True ) assert b"send_confirmation_form" in response.data assert b"jyl@lp.com" in response.data @pytest.mark.registerable() @pytest.mark.settings(confirm_email_within="1 minutes") @pytest.mark.filterwarnings("ignore:.*Non timedelta") def test_expired_confirmation_token(client, get_message): # Note that we need relatively new-ish date since session cookies also expire. with freeze_time(date.today() + timedelta(days=-1)): with capture_registrations() as registrations: data = dict( email="mary@lp.com", password="awesome sunset", password_confirm="awesome sunset", next="", ) client.post("/register", data=data, follow_redirects=True) email = registrations[0]["email"] token = registrations[0]["confirm_token"] response = client.get("/confirm/" + token, follow_redirects=True) msg = get_message("CONFIRMATION_EXPIRED", within="1 minute", email=email) assert msg in response.data @pytest.mark.registerable() def test_email_conflict_for_confirmation_token(app, client, get_message): with capture_registrations() as registrations: data = dict( email="mary@lp.com", password="awesome sunset", password_confirm="awesome sunset", next="", ) client.post("/register", data=data, follow_redirects=True) user = registrations[0]["user"] token = registrations[0]["confirm_token"] # Change the user's email user.email = "tom@lp.com" with app.app_context(): app.security.datastore.put(user) app.security.datastore.commit() response = client.get("/confirm/" + token, follow_redirects=True) msg = get_message("INVALID_CONFIRMATION_TOKEN") assert msg in response.data @pytest.mark.registerable() @pytest.mark.settings(login_without_confirmation=True) def test_login_when_unconfirmed(client, get_message): data = dict( email="mary@lp.com", password="awesome sunset", password_confirm="awesome sunset", next="", ) response = client.post("/register", data=data, follow_redirects=True) assert b"mary@lp.com" in response.data @pytest.mark.registerable() @pytest.mark.settings(password_confirm_required=False) def test_no_auth_token(client_nc): """Make sure that register doesn't return Authentication Token if user isn't confirmed. """ response = client_nc.post( "/register?include_auth_token", json=dict(email="dude@lp.com", password="awesome sunset"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 user = response.json["response"]["user"] assert len(user) == 2 and all(k in user for k in ["email", "last_update"]) @pytest.mark.registerable() @pytest.mark.settings(login_without_confirmation=True, password_confirm_required=False) def test_auth_token_unconfirmed(client_nc): """Make sure that register returns Authentication Token if user isn't confirmed, but the 'login_without_confirmation' flag is set. """ response = client_nc.post( "/register?include_auth_token", json=dict(email="dude@lp.com", password="awesome sunset"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 user = response.json["response"]["user"] assert len(user) == 3 and all( k in user for k in ["email", "last_update", "authentication_token"] ) @pytest.mark.registerable() @pytest.mark.settings( login_without_confirmation=True, auto_login_after_confirm=False, password_confirm_required=False, ) def test_confirmation_different_user_when_logged_in_no_auto(client, get_message): """Default - AUTO_LOGIN == false so shouldn't log in second user.""" e1 = "dude@lp.com" e2 = "lady@lp.com" with capture_registrations() as registrations: for e in e1, e2: data = dict(email=e, password="awesome sunset", next="") client.post("/register", data=data) logout(client) token1 = registrations[0]["confirm_token"] token2 = registrations[1]["confirm_token"] client.get("/confirm/" + token1, follow_redirects=True) logout(client) authenticate(client, email=e1) response = client.get("/confirm/" + token2, follow_redirects=True) assert get_message("EMAIL_CONFIRMED") in response.data # should get a login view assert b'Login' in response.data @pytest.mark.registerable() @pytest.mark.settings(login_without_confirmation=True) def test_confirmation_different_user_when_logged_in(client, get_message): # with default no-auto-login - first user should get logged out and second # should be properly confirmed (but not logged in) e1 = "dude@lp.com" e2 = "lady@lp.com" with capture_registrations() as registrations: for e in e1, e2: data = dict( email=e, password="awesome sunset", password_confirm="awesome sunset", next="", ) response = client.post("/register", data=data) assert is_authenticated(client, get_message) logout(client) token1 = registrations[0]["confirm_token"] token2 = registrations[1]["confirm_token"] response = client.get("/confirm/" + token1, follow_redirects=False) assert "/login" in response.location authenticate(client, email=e1, password="awesome sunset") assert is_authenticated(client, get_message) response = client.get("/confirm/" + token2, follow_redirects=True) assert get_message("EMAIL_CONFIRMED") in response.data # first user should have been logged out assert not is_authenticated(client, get_message) authenticate(client, email=e2, password="awesome sunset") assert is_authenticated(client, get_message) @pytest.mark.registerable() @pytest.mark.settings(recoverable=True, password_confirm_required=False) def test_cannot_reset_password_when_email_is_not_confirmed(client, get_message): email = "dude@lp.com" data = dict(email=email, password="awesome sunset", next="") response = client.post("/register", data=data, follow_redirects=True) response = client.post("/reset", data=dict(email=email), follow_redirects=True) assert get_message("CONFIRMATION_REQUIRED") in response.data @pytest.mark.registerable() @pytest.mark.settings(auto_login_after_confirm=False, password_confirm_required=False) def test_confirm_redirect(client, get_message): with capture_registrations() as registrations: data = dict(email="jane@lp.com", password="awesome sunset", next="") client.post("/register", data=data, follow_redirects=True) token = registrations[0]["confirm_token"] response = client.get("/confirm/" + token) assert "location" in response.headers assert "/login" in response.location response = client.get(response.location) assert get_message("EMAIL_CONFIRMED") in response.data @pytest.mark.registerable() @pytest.mark.settings( post_confirm_view="/post_confirm", password_confirm_required=False ) def test_confirm_redirect_to_post_confirm(client, get_message): with capture_registrations() as registrations: data = dict(email="john@lp.com", password="awesome sunset", next="") client.post("/register", data=data, follow_redirects=True) token = registrations[0]["confirm_token"] response = client.get("/confirm/" + token, follow_redirects=False) assert "/post_confirm" in response.location @pytest.mark.registerable() @pytest.mark.settings( redirect_host="localhost:8081", redirect_behavior="spa", post_confirm_view="/confirm-redirect", confirm_error_view="/confirm-error", password_confirm_required=False, ) def test_spa_get(app, client, get_message): """ Test 'single-page-application' style redirects This uses json only. """ with capture_flashes() as flashes: with capture_registrations() as registrations: response = client.post( "/register", json=dict(email="dude@lp.com", password="awesome sunset"), headers={"Content-Type": "application/json"}, ) assert response.headers["Content-Type"] == "application/json" token = registrations[0]["confirm_token"] response = client.get("/confirm/" + token) assert response.status_code in [302, 303] split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/confirm-redirect" == split.path qparams = dict(parse_qsl(split.query)) assert qparams["email"] == "dude@lp.com" response = client.get("/confirm/" + token) split = urlsplit(response.headers["Location"]) qparams = dict(parse_qsl(split.query)) assert response.status_code in [302, 303] assert "/confirm-error" in response.location assert "email" not in qparams assert get_message("ALREADY_CONFIRMED") in qparams["info"].encode("utf-8") # Arguably for json we shouldn't have any - this is buried in register_user # but really shouldn't be. assert len(flashes) == 1 @pytest.mark.registerable() @pytest.mark.settings( confirm_email_within=timedelta(minutes=300), redirect_host="localhost:8081", redirect_behavior="spa", confirm_error_view="/confirm-error", password_confirm_required=False, ) def test_spa_get_bad_token(app, client, get_message): """Test expired and invalid token""" with capture_flashes() as flashes: # Note that we need relatively new-ish date since session cookies also expire. with freeze_time(date.today() + timedelta(days=-1)): with capture_registrations() as registrations: response = client.post( "/register", json=dict(email="dude@lp.com", password="awesome sunset"), headers={"Content-Type": "application/json"}, ) assert response.headers["Content-Type"] == "application/json" token = registrations[0]["confirm_token"] response = client.get("/confirm/" + token) assert response.status_code in [302, 303] split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/confirm-error" == split.path qparams = dict(parse_qsl(split.query)) assert "email" not in qparams assert "identity" not in qparams msg = get_message("CONFIRMATION_EXPIRED", within="5 hours") assert msg == qparams["error"].encode("utf-8") # Test mangled token token = ( "WyIxNjQ2MzYiLCIxMzQ1YzBlZmVhM2VhZjYwODgwMDhhZGU2YzU0MzZjMiJd." "BZEw_Q.lQyo3npdPZtcJ_sNHVHP103syjM" "&url_id=fbb89a8328e58c181ea7d064c2987874bc54a23d" ) response = client.get("/confirm/" + token) assert response.status_code in [302, 303] split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/confirm-error" == split.path qparams = dict(parse_qsl(split.query)) assert len(qparams) == 1 assert all(k in qparams for k in ["error"]) msg = get_message("INVALID_CONFIRMATION_TOKEN") assert msg == qparams["error"].encode("utf-8") assert len(flashes) == 1 @pytest.mark.filterwarnings("ignore") @pytest.mark.registerable() @pytest.mark.settings(auto_login_after_confirm=True, post_login_view="/postlogin") def test_auto_login(app, client, get_message): with capture_registrations() as registrations: data = dict( email="mary@lp.com", password="password", password_confirm="password", next="", ) client.post("/register", data=data, follow_redirects=True) assert not is_authenticated(client, get_message) token = registrations[0]["confirm_token"] response = client.get("/confirm/" + token, follow_redirects=False) assert check_location(app, response.location, "/postlogin") assert is_authenticated(client, get_message) @pytest.mark.filterwarnings("ignore") @pytest.mark.two_factor() @pytest.mark.registerable() @pytest.mark.settings(two_factor_required=True, auto_login_after_confirm=True) def test_two_factor(app, client, get_message): """If two-factor is enabled, the confirm shouldn't login, but start the 2-factor setup. """ with capture_registrations() as registrations: data = dict( email="mary@lp.com", password="password", password_confirm="password", next="", ) client.post("/register", data=data, follow_redirects=True) assert not is_authenticated(client, get_message) token = registrations[0]["confirm_token"] response = client.get("/confirm/" + token, follow_redirects=False) assert "tf-setup" in response.location @pytest.mark.filterwarnings("ignore") @pytest.mark.two_factor() @pytest.mark.registerable() @pytest.mark.settings(two_factor_required=True, auto_login_after_confirm=True) def test_two_factor_json(app, client, get_message): with capture_registrations() as registrations: data = dict( email="dude@lp.com", password="password", password_confirm="password" ) response = client.post("/register", content_type="application/json", json=data) assert response.headers["content-type"] == "application/json" assert response.json["meta"]["code"] == 200 assert len(response.json["response"]) == 2 assert all(k in response.json["response"] for k in ["csrf_token", "user"]) assert not is_authenticated(client, get_message) token = registrations[0]["confirm_token"] response = client.get("/confirm/" + token, headers={"Accept": "application/json"}) assert response.status_code == 200 assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "setup_from_login" @pytest.mark.registerable() @pytest.mark.settings( user_identity_attributes=[{"username": {"mapper": lambda x: x}}], username_enable=True, password_confirm_required=False, ) def test_email_not_identity(app, client, get_message): # Test that can register/confirm with email even if it isn't an IDENTITY_ATTRIBUTE with capture_registrations() as registrations: data = dict(email="mary2@lp.com", username="mary", password="awesome sunset") response = client.post("/register", data=data, follow_redirects=True) assert b"mary2@lp.com" in response.data token = registrations[0]["confirm_token"] response = client.get("/confirm/" + token, headers={"Accept": "application/json"}) assert response.status_code in [302, 303] assert not is_authenticated(client, get_message) # check that username must be unique data = dict(email="mary4@lp.com", username="mary", password="awesome sunset") response = client.post( "/register", data=data, headers={"Accept": "application/json"} ) assert response.status_code == 400 assert "is already associated" in response.json["response"]["errors"][0] # verify that email field not present response = client.get("/login") # this one has a flash containing 'email confirmed' response = client.get("/login") assert b"email" not in response.data assert b"username" in response.data response = client.get("/login", headers={"Content-Type": "application/json"}) assert response.json["response"]["identity_attributes"] == ["username"] # log in with username response = client.post( "/login", data=dict(username="mary", password="awesome sunset"), follow_redirects=True, ) assert b"

    Welcome mary

    " in response.data @pytest.mark.settings(return_generic_responses=True) def test_generic_response(app, client, get_message): # Confirm matt - then an unknown email - both should get the same answer and # JSON should return 200 recorded_instructions_sent = [] @confirm_instructions_sent.connect_via(app) def on_instructions_sent(app, **kwargs): recorded_instructions_sent.append(kwargs["token"]) response = client.post("/confirm", data=dict(email="matt@lp.com")) assert len(recorded_instructions_sent) == 1 assert get_message("CONFIRMATION_REQUEST", email="matt@lp.com") in response.data response = client.post("/confirm", json=dict(email="matt@lp.com")) assert len(recorded_instructions_sent) == 2 assert response.status_code == 200 # actually confirm matt token = recorded_instructions_sent[0] response = client.get("/confirm/" + token, follow_redirects=True) assert get_message("EMAIL_CONFIRMED") in response.data # Try to confirm an unknown email - should get SAME message as real email. response = client.post("/confirm", data=dict(email="mattwho@lp.com")) assert len(recorded_instructions_sent) == 2 assert get_message("CONFIRMATION_REQUEST", email="mattwho@lp.com") in response.data response = client.post("/confirm", json=dict(email="mattwho@lp.com")) assert len(recorded_instructions_sent) == 2 assert response.status_code == 200 assert not any(e in response.json["response"].keys() for e in ["error", "errors"]) # Try to confirm matt again - should ALSO get same response. response = client.post("/confirm", json=dict(email="matt@lp.com")) assert len(recorded_instructions_sent) == 2 assert response.status_code == 200 def test_generic_with_extra(app, sqlalchemy_datastore): # If application adds a field, make sure we properly return errors # even if 'RETURN_GENERIC_RESPONSES' is set. class MySendConfirmationForm(SendConfirmationForm): recaptcha = StringField("Recaptcha", validators=[Length(min=5)]) app.config["SECURITY_RETURN_GENERIC_RESPONSES"] = True app.config["SECURITY_SEND_CONFIRMATION_TEMPLATE"] = "generic_confirm.html" app.security = Security( app, datastore=sqlalchemy_datastore, send_confirmation_form=MySendConfirmationForm, ) populate_data(app) client = app.test_client() # Test valid user but invalid additional form field # We should get a form error for the extra (invalid) field, no flash bad_data = dict(email="joe@lp.com", recaptcha="1234") good_data = dict(email="joe@lp.com", recaptcha="123456") with capture_flashes() as flashes: response = client.post("/confirm", data=bad_data) assert b"Field must be at least 5" in response.data assert len(flashes) == 0 with capture_flashes() as flashes: response = client.post("/confirm", data=good_data) assert len(flashes) == 1 # JSON with capture_flashes() as flashes: response = client.post("/confirm", json=bad_data) assert response.status_code == 400 assert ( "Field must be at least 5" in response.json["response"]["field_errors"]["recaptcha"][0] ) assert len(flashes) == 0 with capture_flashes() as flashes: response = client.post("/confirm", json=good_data) assert response.status_code == 200 assert len(flashes) == 0 # Try bad email AND bad recaptcha bad_data = dict(email="joe44-lp.com", recaptcha="1234") with capture_flashes() as flashes: response = client.post("/confirm", data=bad_data) assert b"Field must be at least 5" in response.data assert len(flashes) == 0 with capture_flashes() as flashes: response = client.post("/confirm", json=bad_data) assert response.status_code == 400 assert ( "Field must be at least 5" in response.json["response"]["field_errors"]["recaptcha"][0] ) assert len(response.json["response"]["errors"]) == 1 assert len(flashes) == 0 @pytest.mark.flask_async() @pytest.mark.registerable() def test_confirmable_async(app, client, get_message): recorded_confirms = [] recorded_instructions_sent = [] @user_confirmed.connect_via(app) async def on_confirmed(myapp, user): recorded_confirms.append(user) @confirm_instructions_sent.connect_via(app) async def on_instructions_sent(myapp, **kwargs): recorded_instructions_sent.append(kwargs["user"]) email = "dude@lp.com" with capture_registrations() as registrations: data = dict( email=email, password="awesome sunset", password_confirm="awesome sunset", next="", ) response = client.post("/register", data=data) assert response.status_code in [302, 303] client.post( "/confirm", json=dict(email=email), headers={"Content-Type": "application/json"}, ) assert len(recorded_instructions_sent) == 1 # Test confirm token = registrations[0]["confirm_token"] client.get("/confirm/" + token, follow_redirects=False) assert len(recorded_confirms) == 1 pallets-eco-flask-security-72bf929/tests/test_context_processors.py000066400000000000000000000225211525721743500257710ustar00rootroot00000000000000""" test_context_processors ~~~~~~~~~~~~~~~~~~~~~~~ Context processor tests """ import pytest from tests.test_two_factor import tf_authenticate from tests.test_unified_signin import authenticate as us_authenticate from tests.test_utils import authenticate, capture_reset_password_requests, logout @pytest.mark.recoverable() @pytest.mark.registerable() @pytest.mark.confirmable() @pytest.mark.changeable() @pytest.mark.change_email() @pytest.mark.username_recovery() @pytest.mark.change_username() @pytest.mark.settings( login_without_confirmation=True, change_email_template="custom_security/change_email.html", change_password_template="custom_security/change_password.html", login_user_template="custom_security/login_user.html", reset_password_template="custom_security/reset_password.html", forgot_password_template="custom_security/forgot_password.html", send_confirmation_template="custom_security/send_confirmation.html", register_user_template="custom_security/register_user.html", verify_template="custom_security/verify.html", username_recovery_template="custom_security/recover_username.html", change_username_template="custom_security/change_username.html", email_template_password_reset="security/email/reset_instructions_test", email_html=False, ) def test_context_processors(client, app, outbox): @app.security.context_processor def default_ctx_processor(): return {"global": "global"} @app.security.forgot_password_context_processor def forgot_password(): return {"foo": "bar-forgot"} response = client.get("/reset") assert b"global" in response.data assert b"bar-forgot" in response.data @app.security.login_context_processor def login(): return {"foo": "bar-login"} response = client.get("/login") assert b"global" in response.data assert b"bar-login" in response.data @app.security.verify_context_processor def verify(): return {"foo": "bar-verify"} authenticate(client) response = client.get("/verify") assert b"CUSTOM VERIFY USER" in response.data assert b"global" in response.data assert b"bar-verify" in response.data logout(client) @app.security.register_context_processor def register(): return {"foo": "bar-register"} response = client.get("/register") assert b"global" in response.data assert b"bar-register" in response.data @app.security.reset_password_context_processor def reset_password(): return {"foo": "bar-reset"} # /reset/token - need to generate a token with capture_reset_password_requests() as requests: response = client.post( "/reset", data=dict(email="joe@lp.com"), follow_redirects=True ) token = requests[0]["token"] response = client.get(f"/reset/{token}") assert b"global" in response.data assert b"bar-reset" in response.data @app.security.change_password_context_processor def change_password(): return {"foo": "bar-change"} authenticate(client) response = client.get("/change") assert b"global" in response.data assert b"bar-change" in response.data @app.security.send_confirmation_context_processor def send_confirmation(): return {"foo": "bar-confirm"} response = client.get("/confirm") assert b"global" in response.data assert b"bar-confirm" in response.data @app.security.mail_context_processor def mail(): return {"foo": "bar-mail"} client.post("/logout") client.post("/reset", data=dict(email="matt@lp.com")) email = outbox[1] assert "global" in email.body assert "bar-mail" in email.body @app.security.change_email_context_processor def change_email(): return {"foo": "bar-change-email"} authenticate(client) response = client.get("/change-email") assert b"global" in response.data assert b"bar-change-email" in response.data @app.security.recover_username_context_processor def recover_username(): return {"foo": "bar-recover-username"} client.post("/logout") response = client.get("/recover-username") assert b"global" in response.data assert b"bar-recover-username" in response.data @app.security.change_username_context_processor def change_username(): return {"foo": "bar-change-username"} authenticate(client) response = client.get("/change-username") assert b"global" in response.data assert b"bar-change-username" in response.data @pytest.mark.passwordless() @pytest.mark.settings(send_login_template="custom_security/send_login.html") def test_passwordless_login_context_processor(app, client): @app.security.send_login_context_processor def send_login(): return {"foo": "bar-send-login"} response = client.get("/login") assert b"bar-send-login" in response.data @pytest.mark.two_factor() @pytest.mark.settings( two_factor_required=True, login_user_template="custom_security/login_user.html", two_factor_setup_template="custom_security/tf_setup.html", two_factor_verify_code_template="custom_security/tf_verify.html", ) def test_two_factor_context_processors(client, app): # Test two factor context processors @app.security.context_processor def default_ctx_processor(): return {"global": "global"} @app.security.tf_setup_context_processor def send_two_factor_setup(): return {"foo": "bar-tfsetup"} # Note this just does initial login on a user that hasn't setup 2FA yet. authenticate(client) response = client.get("/tf-setup") assert b"global" in response.data assert b"bar-tfsetup" in response.data logout(client) @app.security.tf_token_validation_context_processor def send_two_factor_token_validation(): return {"foo": "bar-tfvalidate"} tf_authenticate(app, client, validate=False) response = client.get("/tf-rescue") assert b"global" in response.data assert b"bar-tfvalidate" in response.data logout(client) @pytest.mark.unified_signin() @pytest.mark.settings( us_setup_template="custom_security/us_setup.html", us_signin_template="custom_security/us_signin.html", us_verify_template="custom_security/us_verify.html", ) def test_unified_signin_context_processors(client, app): @app.security.context_processor def default_ctx_processor(): return {"global": "global"} @app.security.us_signin_context_processor def signin_ctx(): return {"foo": "signin"} # signin template is used in 3 places (TODO test POST us-send-code) response = client.get("/us-signin") assert b"CUSTOM UNIFIED SIGN IN" in response.data assert b"global" in response.data assert b"signin" in response.data response = client.post("/us-signin/send-code") assert b"CUSTOM UNIFIED SIGN IN" in response.data assert b"global" in response.data assert b"signin" in response.data @app.security.us_setup_context_processor def setup_ctx(): return {"foo": "setup"} us_authenticate(client) response = client.get("us-setup") assert b"CUSTOM UNIFIED SIGNIN SETUP" in response.data assert b"global" in response.data assert b"setup" in response.data response = client.post("us-setup", data=dict(chosen_method="sms", phone="555-1212")) assert b"CUSTOM UNIFIED SIGNIN SETUP" in response.data assert b"global" in response.data assert b"setup" in response.data @app.security.us_verify_context_processor def verify_ctx(): return {"foo": "setup"} us_authenticate(client) response = client.get("us-verify") assert b"CUSTOM UNIFIED VERIFY" in response.data assert b"global" in response.data assert b"setup" in response.data response = client.post( "us-verify", data=dict(chosen_method="sms", phone="555-1212") ) assert b"CUSTOM UNIFIED VERIFY" in response.data assert b"global" in response.data assert b"setup" in response.data @pytest.mark.two_factor() @pytest.mark.settings( multi_factor_recovery_template="custom_security/mf_recovery.html", multi_factor_recovery_codes_template="custom_security/mf_recovery_codes.html", multi_factor_recovery_codes=True, ) def test_mf_recovery_context_processors(client, app): @app.security.context_processor def default_ctx_processor(): return {"global": "global"} @app.security.mf_recovery_codes_context_processor def codes_ctx(): return {"foo": "codes"} authenticate(client) response = client.get("/mf-recovery-codes") assert b"global" in response.data assert b"codes" in response.data logout(client) @app.security.mf_recovery_context_processor def code_ctx(): return {"foo": "code"} authenticate(client, "gal@lp.com") response = client.get("/mf-recovery") assert b"global" in response.data assert b"code" in response.data @pytest.mark.settings( logout_user_template="custom_security/logout_user.html", logout_csrf=True ) @pytest.mark.csrf(ignore_unauth=True) def test_logout_context_processor(app, client): @app.security.context_processor def default_ctx_processor(): return {"global": "global"} @app.security.logout_context_processor def logout_ctx(): return {"foo": "bar-logout"} authenticate(client) response = client.post("/logout") assert b"global" in response.data assert b"bar-logout" in response.data pallets-eco-flask-security-72bf929/tests/test_csrf.py000066400000000000000000000620211525721743500227570ustar00rootroot00000000000000""" test_csrf ~~~~~~~~~~~~~~~~~ CSRF tests :copyright: (c) 2019-2024 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from contextlib import contextmanager from datetime import date, timedelta import flask_wtf.csrf import pytest from flask_wtf import CSRFProtect from freezegun import freeze_time from flask import render_template_string from flask_security import Security, auth_required from tests.test_utils import ( get_form_input_value, get_session, is_authenticated, logout, authenticate, ) REAL_VALIDATE_CSRF = None @contextmanager def mp_validate_csrf(): """Make sure we are really calling CSRF validation and getting correct answer""" orig_validate_csrf = flask_wtf.csrf.validate_csrf try: mp = MpValidateCsrf(orig_validate_csrf) flask_wtf.csrf.validate_csrf = mp.mp_validate_csrf yield mp finally: flask_wtf.csrf.validate_csrf = orig_validate_csrf class MpValidateCsrf: success = 0 failure = 0 def __init__(self, real_validate_csrf): MpValidateCsrf.success = 0 MpValidateCsrf.failure = 0 global REAL_VALIDATE_CSRF REAL_VALIDATE_CSRF = real_validate_csrf @staticmethod def mp_validate_csrf(data, secret_key=None, time_limit=None, token_key=None): try: REAL_VALIDATE_CSRF(data, secret_key, time_limit, token_key) MpValidateCsrf.success += 1 except Exception: MpValidateCsrf.failure += 1 raise def _get_csrf_token(client): response = client.get( "/login", data={}, headers={"Content-Type": "application/json"} ) return response.json["response"]["csrf_token"] def json_login( client, email="matt@lp.com", password="password", endpoint=None, use_header=False, remember=None, ): # Return tuple (auth_token, csrf_token) csrf_token = _get_csrf_token(client) data = dict(email=email, password=password, remember=remember) if use_header: headers = {"X-CSRF-Token": csrf_token} else: headers = {} data["csrf_token"] = csrf_token response = client.post( endpoint or "/login?include_auth_token", content_type="application/json", json=data, headers=headers, ) assert response.status_code == 200 rd = response.json["response"] return rd["user"]["authentication_token"], rd["csrf_token"] def json_logout(client): response = client.post("logout", content_type="application/json", data={}) assert response.status_code == 200 assert response.json["meta"]["code"] == 200 return response @pytest.mark.csrf() def test_login_csrf(app, client): # This shouldn't log in - but return login form with csrf token. data = dict(email="matt@lp.com", password="password", remember="y") response = client.post("/login", data=data) assert response.status_code == 200 assert b"The CSRF token is missing." in response.data data["csrf_token"] = get_form_input_value(response, "csrf_token") response = client.post("/login", data=data, follow_redirects=True) assert response.status_code == 200 assert b"Welcome matt" in response.data response = logout(client, follow_redirects=True) assert response.status_code == 200 assert b"Log in" in response.data def test_login_csrf_double(app, client): # Test if POST login while already logged in - just redirects to POST_LOGIN app.config["WTF_CSRF_ENABLED"] = True # This shouldn't log in - but return login form with csrf token. data = dict(email="matt@lp.com", password="password", remember="y") response = client.post("/login", data=data) assert response.status_code == 200 assert b"csrf_token" in response.data data["csrf_token"] = _get_csrf_token(client) response = client.post("/login", data=data, follow_redirects=True) assert response.status_code == 200 assert b"Welcome matt" in response.data data["csrf_token"] = _get_csrf_token(client) # Note - should redirect to POST_LOGIN with current user ignoring form data. data["email"] = "newguy@me.com" response = client.post("/login", data=data, follow_redirects=True) assert response.status_code == 200 assert b"Welcome matt" in response.data @pytest.mark.csrf() def test_login_csrf_json(app, client): with mp_validate_csrf() as mp: auth_token, csrf_token = json_login(client) assert auth_token assert csrf_token # Should be just one call to validate - since CSRFProtect not enabled. assert mp.success == 1 and mp.failure == 0 response = json_logout(client) session = get_session(response) assert "csrf_token" not in session @pytest.mark.csrf(csrfprotect=True) def test_login_csrf_json_header(app, client): with mp_validate_csrf() as mp: auth_token, csrf_token = json_login(client, use_header=True) assert auth_token assert csrf_token assert mp.success == 1 and mp.failure == 0 json_logout(client) @pytest.mark.settings(csrf_ignore_unauth_endpoints=True) def test_login_csrf_unauth_ok(app, client): app.config["WTF_CSRF_ENABLED"] = True with mp_validate_csrf() as mp: # This should log in. data = dict(email="matt@lp.com", password="password", remember="y") response = client.post("/login", data=data, follow_redirects=True) assert response.status_code == 200 assert b"Welcome matt" in response.data assert mp.success == 0 and mp.failure == 0 logout(client) @pytest.mark.settings(csrf_ignore_unauth_endpoints=True) def test_login_csrf_unauth_double(app, client, get_message): # Test double login w/o CSRF returns unauth required error message. app.config["WTF_CSRF_ENABLED"] = True # This should log in. data = dict(email="matt@lp.com", password="password", remember="y") response = client.post("/login", data=data, follow_redirects=True) assert response.status_code == 200 assert b"Welcome matt" in response.data # login in again - should work response = client.post("/login", content_type="application/json", json=data) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "ANONYMOUS_USER_REQUIRED" ) @pytest.mark.csrf() @pytest.mark.recoverable() def test_reset(app, client): """Test that form-based CSRF works for /reset""" response = client.get("/reset", content_type="application/json") csrf_token = response.json["response"]["csrf_token"] with mp_validate_csrf() as mp: data = dict(email="matt@lp.com") # should fail - no CSRF token - should get a JSON response response = client.post("/reset", content_type="application/json", json=data) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." # test template also has error - since using just Flask-WTF form based CSRF - # should be an error on the csrf_token field. response = client.post("/reset", data=data) assert b'class="fs-error-msg">The CSRF token is missing' in response.data # test sending csrf_token works - JSON data["csrf_token"] = csrf_token response = client.post("/reset", content_type="application/json", json=data) assert response.status_code == 200 # test sending csrf_token works - forms response = client.post("/reset", data=data) assert b"Send password reset instructions" in response.data assert mp.success == 2 and mp.failure == 2 @pytest.mark.recoverable() @pytest.mark.csrf(csrfprotect=True) def test_cp_reset(app, client): """Test that header based CSRF works for /reset when using WTF_CSRF_CHECK_DEFAULT=False. """ with mp_validate_csrf() as mp: data = dict(email="matt@lp.com") # should fail - no CSRF token response = client.post("/reset", content_type="application/json", json=data) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." csrf_token = _get_csrf_token(client) response = client.post( "/reset", content_type="application/json", json=data, headers={"X-CSRF-Token": csrf_token}, ) assert response.status_code == 200 assert mp.success == 1 and mp.failure == 1 @pytest.mark.changeable() @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings(csrf_header="X-XSRF-Token") def test_cp_with_token(app, client): # Make sure can use returned CSRF-Token in Header. # Since the csrf token isn't in the form - must enable app-wide CSRF # using CSRFProtect() - as the above mark does. # Using X-XSRF-Token as header tests that we properly # add that as a known header to WTFforms. auth_token, csrf_token = json_login(client, use_header=True) # make sure returned csrf_token works in header. data = dict( password="password", new_password="battery staple", new_password_confirm="battery staple", ) with mp_validate_csrf() as mp: response = client.post( "/change", content_type="application/json", json=data, headers={"X-XSRF-Token": csrf_token}, ) assert response.status_code == 200 assert mp.success == 1 and mp.failure == 0 json_logout(client) def test_cp_login_json_no_session(app, sqlalchemy_datastore): # Test with global CSRFProtect on and not sending cookie - nothing works. app.config["WTF_CSRF_ENABLED"] = True CSRFProtect(app) app.security = Security(app=app, datastore=sqlalchemy_datastore) client_nc = app.test_client(use_cookies=False) # This shouldn't log in - and will return 400 with mp_validate_csrf() as mp: data = dict(email="matt@lp.com", password="password", remember="y") response = client_nc.post( "/login", content_type="application/json", json=data, headers={"Accept": "application/json"}, ) assert response.status_code == 400 # This still wont work since we don't send a session cookie response = client_nc.post( "/login", content_type="application/json", json=data, headers={"X-CSRF-Token": _get_csrf_token(client_nc)}, ) assert response.status_code == 400 # Although failed - CSRF should have been called assert mp.failure == 2 @pytest.mark.settings(CSRF_PROTECT_MECHANISMS=["basic", "session"]) def test_cp_config(app, sqlalchemy_datastore): # Test improper config (must have WTF_CSRF_CHECK_DEFAULT false if setting # CSRF_PROTECT_MECHANISMS from flask_security import Security app.config["WTF_CSRF_ENABLED"] = True CSRFProtect(app) # The check is done on first request. with pytest.raises(ValueError) as ev: Security(app=app, datastore=sqlalchemy_datastore) assert "must be set to False" in str(ev.value) @pytest.mark.settings(CSRF_PROTECT_MECHANISMS=["basic", "session"]) def test_cp_config2(app, sqlalchemy_datastore): # Test improper config (must have CSRFProtect configured if setting # CSRF_PROTECT_MECHANISMS from flask_security import Security app.config["WTF_CSRF_ENABLED"] = True with pytest.raises(ValueError) as ev: Security(app=app, datastore=sqlalchemy_datastore) assert "CsrfProtect not part of application" in str(ev.value) @pytest.mark.changeable() @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings(CSRF_PROTECT_MECHANISMS=["basic", "session"]) def test_different_mechanisms(app, client): # Verify that using token doesn't require CSRF, but sessions do with mp_validate_csrf() as mp: auth_token, csrf_token = json_login(client, use_header=True) # session based change password should fail data = dict( password="password", new_password="battery staple", new_password_confirm="battery staple", ) response = client.post( "/change", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert b"The CSRF token is missing" in response.data # token based should work response = client.post( "/change", json=data, headers={ "Content-Type": "application/json", "Authentication-Token": auth_token, }, ) assert response.status_code == 200 assert mp.success == 1 and mp.failure == 1 @pytest.mark.changeable() @pytest.mark.settings( CSRF_PROTECT_MECHANISMS=["basic", "session"], csrf_ignore_unauth_endpoints=True ) def test_different_mechanisms_nc(app, client_nc): # Verify that using token and no session cookie works # Note that we had to disable unauth_endpoints since you can't log in # w/ CSRF if you don't send in the session cookie. app.config["WTF_CSRF_ENABLED"] = True app.config["WTF_CSRF_CHECK_DEFAULT"] = False CSRFProtect(app) with mp_validate_csrf() as mp: auth_token, csrf_token = json_login(client_nc) # token based should work data = dict( password="password", new_password="battery staple", new_password_confirm="battery staple", ) response = client_nc.post( "/change", json=data, headers={ "Content-Type": "application/json", "Authentication-Token": auth_token, }, ) assert response.status_code == 200 assert mp.success == 0 and mp.failure == 0 @pytest.mark.changeable() @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings(csrf_protect_mechanisms=[]) def test_cp_with_token_empty_mechanisms(app, client): # If no mechanisms - shouldn't do any CSRF auth_token, csrf_token = json_login(client, use_header=True) # make sure returned csrf_token works in header. data = dict( password="password", new_password="battery staple", new_password_confirm="battery staple", ) response = client.post( "/change", content_type="application/json", json=data, headers={ "Content-Type": "application/json", "Authentication-Token": auth_token, }, ) assert response.status_code == 200 @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings(csrf_ignore_unauth_endpoints=True, CSRF_COOKIE_NAME="XSRF-Token") def test_csrf_cookie(app, client): json_login(client) assert client.get_cookie("XSRF-Token") # Make sure cleared on logout response = client.post("/logout", content_type="application/json") assert response.status_code == 200 assert not client.get_cookie("XSRF-Token") @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings(CSRF_COOKIE={"key": "XSRF-Token"}) @pytest.mark.changeable() def test_cp_with_token_cookie(app, client): # Make sure can use returned CSRF-Token cookie in Header when changing password json_login(client, use_header=True) # make sure returned csrf_token works in header. data = dict( password="password", new_password="battery staple", new_password_confirm="battery staple", ) csrf_token = client.get_cookie("XSRF-Token") with mp_validate_csrf() as mp: response = client.post( "/change", content_type="application/json", json=data, headers={"X-XSRF-Token": csrf_token.value}, ) assert response.status_code == 200 assert mp.success == 1 and mp.failure == 0 json_logout(client) assert not client.get_cookie("XSRF-Token") @pytest.mark.csrf(csrfprotect=True) @pytest.mark.app_settings(wtf_csrf_time_limit=1) @pytest.mark.settings(CSRF_COOKIE_NAME="XSRF-Token", csrf_ignore_unauth_endpoints=True) @pytest.mark.changeable() def test_cp_with_token_cookie_expire(app, client): # Make sure that we get a new Csrf-Token cookie if expired. # Note that we need relatively new-ish date since session cookies also expire. with freeze_time(date.today() + timedelta(days=-1)): json_login(client, use_header=True) # time unfrozen so should be expired data = dict( password="password", new_password="battery staple", new_password_confirm="battery staple", ) csrf_token = client.get_cookie("XSRF-Token") with mp_validate_csrf() as mp: response = client.post( "/change", content_type="application/json", json=data, headers={"X-XSRF-Token": csrf_token.value}, ) assert response.status_code == 400 assert b"expired" in response.data # Should have gotten a new CSRF cookie value new_csrf_token = client.get_cookie("XSRF-Token") assert csrf_token.value != new_csrf_token.value # 2 failures since the utils:csrf_cookie_handler will check assert mp.success == 0 and mp.failure == 2 json_logout(client) assert not client.get_cookie("XSRF-Token") @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings( CSRF_COOKIE_NAME="XSRF-Token", CSRF_COOKIE_REFRESH_EACH_REQUEST=True ) @pytest.mark.changeable() def test_cp_with_token_cookie_refresh(app, client): # Test CSRF_COOKIE_REFRESH_EACH_REQUEST json_login(client, use_header=True) # make sure returned csrf_token works in header. data = dict( password="password", new_password="battery staple", new_password_confirm="battery staple", ) csrf_cookie = client.get_cookie("XSRF-Token") with mp_validate_csrf() as mp: # Delete cookie - we should always get a new one client.delete_cookie("XSRF-Token") response = client.post( "/change", content_type="application/json", json=data, headers={"X-XSRF-Token": csrf_cookie.value}, ) assert response.status_code == 200 assert client.get_cookie("XSRF-Token") assert mp.success == 1 and mp.failure == 0 # delete cookie again, do a 'GET' - the REFRESH_COOKIE_ON_EACH_REQUEST should # send us a new one client.delete_cookie("XSRF-Token") response = client.get("/change") assert response.status_code == 200 assert client.get_cookie("XSRF-Token") json_logout(client) assert not client.get_cookie("XSRF-Token") @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings(CSRF_COOKIE_NAME="XSRF-Token") @pytest.mark.changeable() def test_remember_login_csrf_cookie(app, client): # Test csrf cookie upon resuming a remember session # Login with remember_token generation json_login(client, use_header=True, remember=True) client.delete_cookie("XSRF-Token") client.delete_cookie("session") # Do a simple get request with the remember_token cookie present assert client.get_cookie("remember_token") response = client.get("/profile") assert response.status_code == 200 assert client.get_cookie("session") assert client.get_cookie("XSRF-Token") # Logout and check that everything cleans up nicely json_logout(client) assert not client.get_cookie("remember_token") assert not client.get_cookie("session") assert not client.get_cookie("XSRF-Token") @pytest.mark.csrf(csrfprotect=True) @pytest.mark.registerable() @pytest.mark.settings(csrf_header="X-CSRF-Token", password_confirm_required=False) def test_json_register_csrf_with_ignore_unauth_set_to_false(app, client): """ Test that you are able to register a user when using the JSON api and the CSRF_IGNORE_UNAUTH_ENDPOINTS is set to False. """ csrf_token = client.get("/login", headers={"Accept": "application/json"}).json[ "response" ]["csrf_token"] email = "eg@testuser.com" data = {"email": email, "password": "password"} response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." response = client.post( "/register", json=data, headers={"Content-Type": "application/json", "X-CSRF-Token": csrf_token}, ) assert response.status_code == 200 assert response.json["response"]["user"]["email"] == email @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings( csrf_protect_mechanisms=["session"], csrf_ignore_unauth_endpoints=True ) def test_myform(app, client): # Create app form - and make sure protect_mechanisms properly skips CSRF # For this test - we don't configure CSRFProtect - just use form CSRF from flask_wtf import FlaskForm from wtforms import StringField class custom_form(FlaskForm): name = StringField("Name") @app.route("/custom", methods=["GET", "POST"]) @auth_required() def custom(): form = custom_form() if form.validate_on_submit(): return render_template_string(f"Nice POST {form.name.data}") return render_template_string( f"Hi {form.name.data}, anything wrong? {form.errors}" ) auth_token, csrf_token = json_login(client, use_header=True) # using session - POST should fail - no CSRF response = client.post("/custom", json={"name": "first POST"}) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." # use CSRF token - should work response = client.post( "/custom", json={"name": "second POST"}, headers={"X-XSRF-Token": csrf_token} ) assert response.status_code == 200 # try with form input response = client.post( "/custom", data={"name": "form POST", "csrf_token": csrf_token} ) assert response.data == b"Nice POST form POST" # now try authenticating via token - shouldn't need CSRF token client_nc = app.test_client(use_cookies=False) response = client_nc.post( "/custom", json={"name": "authtoken POST"}, headers={ "Content-Type": "application/json", "Authentication-Token": auth_token, }, ) assert b"CSRF" not in response.data @pytest.mark.csrf(csrfprotect=True) def test_csrf_json_protect(app, client): # test sending CSRF token in json body for an unauth endpoint (/login) # In older code the @unauth_csrf() decorator would 'fall through' - if the # decorator CSRF checked failed it would fall through to the form CSRF check. # The decorator CSRF check returns a 400 JSON response. csrf_token = _get_csrf_token(client) response = client.post( "/login", json=dict(email="matt@lp.com", password="password", csrf_token=csrf_token), ) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." @pytest.mark.settings(logout_csrf=True, post_logout_view="/post-csrf-logout") @pytest.mark.csrf() def test_logout_csrf_form(app, client, get_message): # test logout CSRF using default (form) config authenticate(client, csrf=True) response = client.post("/logout") assert b"Confirm Sign Out" in response.data data = dict(csrf_token=get_form_input_value(response, "csrf_token")) response = client.post("/logout", data=data, follow_redirects=False) assert response.location == "/post-csrf-logout" assert not is_authenticated(client, get_message) @pytest.mark.settings(logout_csrf=True) @pytest.mark.csrf() def test_logout_csrf_json(app, client, get_message): # test logout CSRF using default config - using json auth_token, csrf_token = json_login(client) response = client.post("/logout", json={}) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." data = dict(csrf_token=csrf_token) response = client.post("/logout", json=data) assert response.status_code == 200 assert not is_authenticated(client, get_message) @pytest.mark.settings(logout_csrf=True, post_logout_view="/post-csrf-logout") @pytest.mark.csrf(csrfprotect=True) def test_logout_csrfapp_form(app, client, get_message): # test logout CSRF using CSRFProtect authenticate(client, csrf=True) response = client.post("/logout") assert b"Confirm Sign Out" in response.data data = dict(csrf_token=get_form_input_value(response, "csrf_token")) response = client.post("/logout", data=data, follow_redirects=False) assert response.location == "/post-csrf-logout" assert not is_authenticated(client, get_message) @pytest.mark.settings(logout_csrf=True, csrf_cookie_name="mycsrf") @pytest.mark.csrf(csrfprotect=True) def test_logout_csrfapp_json(app, client, get_message): # test logout CSRF using default config - using json authenticate(client, csrf=True) csrf_cookie = client.get_cookie("mycsrf") response = client.post("/logout", json={}) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." headers = {"X-XSRF-Token": csrf_cookie.value} response = client.post("/logout", json={}, headers=headers) assert response.status_code == 200 assert not is_authenticated(client, get_message) pallets-eco-flask-security-72bf929/tests/test_datastore.py000066400000000000000000000622011525721743500240100ustar00rootroot00000000000000""" test_datastore ~~~~~~~~~~~~~~ Datastore tests :copyright: (c) 2012 by Matt Wright. :copyright: (c) 2019-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from datetime import timedelta import pytest from pytest import raises, skip, importorskip from tests.test_utils import init_app_with_options, capture_queries from flask_security import ( RoleMixin, Security, UserMixin, LoginForm, RegisterFormV2, naive_utcnow, ) from flask_security.datastore import Datastore, UserDatastore class User(UserMixin): pass class Role(RoleMixin): pass class MockDatastore(UserDatastore): def put(self, model): pass def delete(self, model): pass def test_unimplemented_datastore_methods(): datastore = Datastore(None) assert datastore.db is None with raises(NotImplementedError): datastore.put(None) with raises(NotImplementedError): datastore.delete(None) assert not datastore.commit() def test_unimplemented_user_datastore_methods(): datastore = UserDatastore(None, None) with raises(NotImplementedError): datastore.find_user() with raises(NotImplementedError): datastore.find_role(None) def test_toggle_active(): datastore = MockDatastore(None, None) user = User() user.active = True assert datastore.toggle_active(user) is True assert not user.active assert datastore.toggle_active(user) is True assert user.active is True def test_deactivate_user(): datastore = MockDatastore(None, None) user = User() user.active = True assert datastore.deactivate_user(user) is True assert not user.active def test_activate_user(): datastore = MockDatastore(None, None) user = User() user.active = False assert datastore.activate_user(user) is True assert user.active is True def test_deactivate_returns_false_if_already_false(): datastore = UserDatastore(None, None) user = User() user.active = False assert not datastore.deactivate_user(user) def test_activate_returns_false_if_already_true(): datastore = UserDatastore(None, None) user = User() user.active = True assert not datastore.activate_user(user) @pytest.mark.parametrize( "ds", ["sqlalchemy_datastore", "fsqlalite_datastore", "sqlalchemy_session_datastore"], ) def test_find_user(request, app, ds): ds = request.getfixturevalue(ds) init_app_with_options(app, ds) with app.app_context(): user_id = ds.find_user(email="gene@lp.com").fs_uniquifier with capture_queries(ds) as queries: assert user_id == ds.find_user(security_number=889900).fs_uniquifier assert len(queries) == 1 assert queries[0].is_select assert user_id == ds.find_user(username="gene").fs_uniquifier @pytest.mark.parametrize( "ds", ["sqlalchemy_datastore", "fsqlalite_datastore", "sqlalchemy_session_datastore"], ) @pytest.mark.settings(join_user_roles=False) def test_find_user_no_joined_load(request, app, ds): ds = request.getfixturevalue(ds) init_app_with_options(app, ds) with app.app_context(): with capture_queries(ds) as queries: user = ds.find_user(security_number=889900) assert len(user.roles) == 1 assert len(queries) == 2 assert queries[0].is_select assert queries[0].statement.column_descriptions[0]["name"] == "User" assert queries[1].statement.column_descriptions[0]["name"] == "Role" def test_find_role(app, datastore): init_app_with_options(app, datastore) with app.app_context(): role = datastore.find_role("admin") assert role is not None role = datastore.find_role("bogus") assert role is None def test_add_role_to_user(app, datastore): init_app_with_options(app, datastore) with app.app_context(): # Test with user object user = datastore.find_user(email="matt@lp.com") assert user.has_role("editor") is False assert datastore.add_role_to_user(user, "editor") is True assert datastore.add_role_to_user(user, "editor") is False assert user.has_role("editor") is True # Test remove role assert datastore.remove_role_from_user(user, "editor") is True assert datastore.remove_role_from_user(user, "editor") is False @pytest.mark.parametrize( "ds", ["sqlalchemy_datastore", "fsqlalite_datastore", "sqlalchemy_session_datastore"], ) def test_create_user_with_roles(request, app, ds): ds = request.getfixturevalue(ds) init_app_with_options(app, ds) with app.app_context(): role = ds.find_role("admin") user = ds.create_user( email="dude@lp.com", username="dude", password="password", roles=[role] ) ds.commit() with capture_queries(ds) as queries: user = ds.find_user(email="dude@lp.com") assert user.has_role("admin") is True assert len(queries) == 1 assert queries[0].is_select def test_create_user_no_side_effects(app, datastore): init_app_with_options(app, datastore) with app.app_context(): datastore.find_role("admin") roles = ["admin"] datastore.commit() datastore.create_user( email="dude@lp.com", username="dude", password="password", roles=roles ) assert all(isinstance(role, str) for role in roles) def test_delete_user(app, datastore): init_app_with_options(app, datastore) with app.app_context(): user = datastore.find_user(email="matt@lp.com") datastore.delete_user(user) datastore.commit() user = datastore.find_user(email="matt@lp.com") assert user is None def test_access_datastore_from_factory(app, datastore): security = Security() security.init_app(app, datastore) assert security.datastore is not None assert security.app is not None def test_access_datastore_from_app_factory_pattern(app, datastore): security = Security(datastore=datastore) security.init_app(app) assert security.datastore is not None assert security.app is not None def test_init_app_kwargs_override_constructor_kwargs(app, datastore): class ConLoginForm(LoginForm): pass class ConRegisterForm(RegisterFormV2): pass class InitLoginForm(LoginForm): pass security = Security( datastore=datastore, login_form=ConLoginForm, register_form=ConRegisterForm, ) security.init_app(app, login_form=InitLoginForm) assert security.forms["login_form"].cls == InitLoginForm assert security.forms["register_form"].cls == ConRegisterForm def test_create_user_with_roles_and_permissions(app, datastore): ds = datastore if not hasattr(ds.role_model, "permissions"): return init_app_with_options(app, ds) with app.app_context(): role = ds.create_role(name="test1", permissions={"read"}) ds.commit() user = ds.create_user( email="dude@lp.com", username="dude", password="password", roles=[role] ) ds.commit() user = ds.find_user(email="dude@lp.com") assert user.has_role("test1") is True assert user.has_permission("read") is True assert user.has_permission("write") is False def test_permissions_types(app, datastore): # Test permissions as a list, set, tuple, comma separated list ds = datastore if not hasattr(ds.role_model, "permissions"): return init_app_with_options(app, ds) with app.app_context(): perms = ["read", "write"] ds.create_role(name="test1", permissions=perms) ds.commit() t1 = ds.find_role("test1") assert {"read", "write"} == t1.get_permissions() perms = {"read", "write"} ds.create_role(name="test2", permissions=perms) ds.commit() t2 = ds.find_role("test2") assert {"read", "write"} == t2.get_permissions() perms = "read, write" ds.create_role(name="test3", permissions=perms) ds.commit() t3 = ds.find_role("test3") assert {"read", "write"} == t3.get_permissions() perms = ("read", "write") ds.create_role(name="test4", permissions=perms) ds.commit() t4 = ds.find_role("test4") assert {"read", "write"} == t4.get_permissions() ds.create_role( name="test5", permissions={"read"}, ) ds.commit() t5 = ds.find_role("test5") assert {"read"} == t5.get_permissions() def test_modify_permissions(app, datastore): ds = datastore if not hasattr(ds.role_model, "permissions"): return init_app_with_options(app, ds) with app.app_context(): perms = {"read", "write"} ds.create_role(name="test1", permissions=perms) ds.commit() t1 = ds.find_role("test1") assert perms == t1.get_permissions() if hasattr(t1, "update_datetime"): orig_update_time = t1.update_datetime assert t1.update_datetime <= naive_utcnow() ds.add_permissions_to_role(t1, "execute") ds.commit() t1 = ds.find_role("test1") assert perms.union({"execute"}) == t1.get_permissions() ds.remove_permissions_from_role(t1, "read") ds.commit() t1 = ds.find_role("test1") assert {"write", "execute"} == t1.get_permissions() if hasattr(t1, "update_datetime"): assert t1.update_datetime > orig_update_time def test_get_permissions(app, datastore): """Verify that role.permissions = None works.""" ds = datastore if not hasattr(ds.role_model, "permissions"): return init_app_with_options(app, ds) with app.app_context(): t1 = ds.find_role("simple") assert set() == t1.get_permissions() def test_modify_permissions_multi(app, datastore): ds = datastore if not hasattr(ds.role_model, "permissions"): return init_app_with_options(app, ds) with app.app_context(): perms = ["read", "write"] ds.create_role(name="test1", permissions=perms) ds.commit() t1 = ds.find_role("test1") assert {"read", "write"} == t1.get_permissions() # send in a list ds.add_permissions_to_role(t1, ["execute", "whatever"]) ds.commit() t1 = ds.find_role("test1") assert {"read", "write", "execute", "whatever"} == t1.get_permissions() ds.remove_permissions_from_role(t1, ["read", "whatever"]) ds.commit() assert {"write", "execute"} == t1.get_permissions() # send in a set perms = {"read", "write"} ds.create_role(name="test2", permissions=perms) ds.commit() # add permissions using comma separate string t2 = ds.find_role("test2") ds.add_permissions_to_role(t2, "execute, whatever") ds.commit() t2 = ds.find_role("test2") assert {"read", "write", "execute", "whatever"} == t2.get_permissions() ds.remove_permissions_from_role(t2, {"read", "whatever"}) ds.commit() assert {"write", "execute"} == t2.get_permissions() ds.remove_permissions_from_role(t2, "write, execute") ds.commit() assert t2.get_permissions() == set() # add permissions using a tuple t2 = ds.find_role("test2") ds.add_permissions_to_role(t2, ("execute2", "whatever2")) ds.commit() assert {"whatever2", "execute2"} == t2.get_permissions() def test_uuid(app, request, tmpdir, realdburl): """Test that UUID extension of postgresql works as a primary id for users""" importorskip("flask_sqlalchemy") import uuid from flask_sqlalchemy import SQLAlchemy from sqlalchemy import Boolean, Column, DateTime, Integer, ForeignKey, String from sqlalchemy.dialects.postgresql import UUID from sqlalchemy.orm import relationship, backref from flask_security import SQLAlchemyUserDatastore from tests.conftest import _setup_realdb, _teardown_realdb # UUID type only supported by postgres - not sqlite. if not realdburl or "postgresql" not in realdburl: skip("This test only works on postgres") db_url, db_info = _setup_realdb(realdburl) app.config["SQLALCHEMY_DATABASE_URI"] = db_url db = SQLAlchemy(app) class RolesUsers(db.Model): __tablename__ = "roles_users" id = Column(Integer(), primary_key=True) user_id = Column("user_id", UUID(as_uuid=True), ForeignKey("user.id")) role_id = Column("role_id", UUID(as_uuid=True), ForeignKey("role.id")) class User(db.Model, UserMixin): __tablename__ = "user" id = Column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True ) email = Column(String(255), unique=True) fs_uniquifier = Column(String(64), unique=True, nullable=False) first_name = Column(String(255), index=True) last_name = Column(String(255), index=True) username = Column(String(255), unique=True, nullable=True) password = Column(String(255)) active = Column(Boolean()) created_at = Column(DateTime, default=naive_utcnow()) confirmed_at = Column(DateTime()) roles = relationship( "Role", secondary="roles_users", backref=backref("users", lazy="dynamic") ) class Role(db.Model, RoleMixin): __tablename__ = "role" id = Column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True ) name = Column(String(80), unique=True) description = Column(String(255)) # __hash__ is required to avoid the exception # TypeError: unhashable type: 'Role' when saving a User def __hash__(self): return hash(self.name) with app.app_context(): db.create_all() def tear_down(): with app.app_context(): db.drop_all() db.engine.dispose() _teardown_realdb(db_info) request.addfinalizer(tear_down) ds = SQLAlchemyUserDatastore(db, User, Role) app.security = Security(app, datastore=ds) with app.app_context(): user = ds.find_user(email="matt@lp.com") assert not user def test_webauthn(app, datastore): importorskip("webauthn") if not datastore.webauthn_model: skip(f"No WebAuthn model defined for datastore: {datastore.__class__.__name__}") init_app_with_options(app, datastore) with app.app_context(): user = datastore.find_user(email="matt@lp.com") datastore.create_webauthn( user, name="cred1", credential_id=b"1", public_key=b"key", sign_count=0, transports=None, extensions=None, usage="first", device_type="single_device", backup_state=False, ) datastore.commit() cred = datastore.find_webauthn(b"1") assert cred.name == "cred1" user = datastore.find_user(email="matt@lp.com") assert len(user.webauthn) == 1 assert user.webauthn[0].name == "cred1" datastore.delete_webauthn(user.webauthn[0]) datastore.commit() user = datastore.find_user(email="matt@lp.com") assert len(user.webauthn) == 0 def test_webauthn_cascade(app, datastore): importorskip("webauthn") if not datastore.webauthn_model: skip(f"No WebAuthn model defined for datastore: {datastore.__class__.__name__}") init_app_with_options(app, datastore) with app.app_context(): user = datastore.find_user(email="matt@lp.com") datastore.create_webauthn( user, name="cred1", credential_id=b"1", public_key=b"key", sign_count=0, transports=None, extensions=None, usage="first", device_type="single_device", backup_state=False, ) datastore.create_webauthn( user, name="cred2", credential_id=b"2", public_key=b"key", sign_count=0, transports=None, extensions=None, usage="secondary", device_type="single_device", backup_state=False, ) datastore.commit() user = datastore.find_user(email="matt@lp.com") assert len(user.webauthn) == 2 names = [cred.name for cred in user.webauthn] assert set(names) == {"cred1", "cred2"} assert datastore.find_webauthn(b"1") # delete user datastore.delete_user(user) datastore.commit() user = datastore.find_user(email="matt@lp.com") assert not user cred = datastore.find_webauthn(b"1") assert not cred cred = datastore.find_webauthn(b"2") assert not cred def test_mf_recovery_codes(app, datastore): init_app_with_options(app, datastore) with app.test_request_context("/"): user = datastore.find_user(email="matt@lp.com") assert hasattr(user, "mf_recovery_codes") assert not datastore.mf_delete_recovery_code(user, 0) datastore.mf_set_recovery_codes(user, ["r1", "r2", "r3"]) datastore.commit() user = datastore.find_user(email="matt@lp.com") codes = datastore.mf_get_recovery_codes(user) assert codes == ["r1", "r2", "r3"] rv = datastore.mf_delete_recovery_code(user, 1) assert rv datastore.commit() rv = datastore.mf_delete_recovery_code(user, 4) assert not rv user = datastore.find_user(email="matt@lp.com") codes = datastore.mf_get_recovery_codes(user) assert codes == ["r1", "r3"] def test_permissions_fsqla_v2(app): importorskip("sqlalchemy") importorskip("flask_sqlalchemy") # Make sure folks with fsqla_v2 work with new AsList column type from sqlalchemy import insert from flask_sqlalchemy import SQLAlchemy from flask_security.models import fsqla_v2 as fsqla from flask_security import Security from flask_security import SQLAlchemyUserDatastore app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" db = SQLAlchemy(app) fsqla.FsModels.set_db_info(db) class Role(db.Model, fsqla.FsRoleMixin): pass class User(db.Model, fsqla.FsUserMixin): pass with app.app_context(): db.create_all() meta_data = db.MetaData() meta_data.reflect(db.engine) role_table = meta_data.tables["role"] # Start by manually creating a role in the 4.1.x style stmt = insert(role_table).values( name="r1", description="r1 v41", permissions="read,write" ) with db.engine.connect() as conn: with conn.begin(): conn.execute(stmt) ds = SQLAlchemyUserDatastore(db, User, Role) app.security = Security(app, datastore=ds) with app.app_context(): # Verify can read something written by 4.x r1 = ds.find_role("r1") assert r1.get_permissions() == {"read", "write"} ds.create_role(name="test5", permissions={"read"}) ds.commit() t5 = ds.find_role("test5") assert {"read"} == t5.get_permissions() with app.app_context(): db.engine.dispose() def test_permissions_41(request, app, realdburl): importorskip("sqlalchemy") importorskip("flask_sqlalchemy") # Check compatibility with 4.1 DB from sqlalchemy import Column, insert from flask_sqlalchemy import SQLAlchemy from flask_security.models import fsqla_v2 as fsqla from flask_security import Security from flask_security import SQLAlchemyUserDatastore from tests.conftest import _setup_realdb, _teardown_realdb if realdburl: db_url, db_info = _setup_realdb(realdburl) app.config["SQLALCHEMY_DATABASE_URI"] = db_url else: app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" def tear_down(): if realdburl: with app.app_context(): db.drop_all() db.engine.dispose() _teardown_realdb(db_info) request.addfinalizer(tear_down) db = SQLAlchemy(app) fsqla.FsModels.set_db_info(db) class Role(db.Model, fsqla.FsRoleMixin): # permissions = Column(UnicodeText, nullable=True) # type: ignore from flask_security import AsaList from sqlalchemy.ext.mutable import MutableList # A comma separated list of strings permissions = Column( MutableList.as_mutable(AsaList()), nullable=True # type: ignore ) class User(db.Model, fsqla.FsUserMixin): pass with app.app_context(): db.create_all() meta_data = db.MetaData() meta_data.reflect(db.engine) role_table = meta_data.tables["role"] # Start by manually creating a role in the 4.1.x style stmt = insert(role_table).values( name="r1", description="r1 v41", permissions="read,write" ) with db.engine.connect() as conn: with conn.begin(): conn.execute(stmt) ds = SQLAlchemyUserDatastore(db, User, Role) app.security = Security(app, datastore=ds) with app.app_context(): r1 = ds.find_role("r1") assert r1.get_permissions() == {"read", "write"} with app.app_context(): db.engine.dispose() def test_fsqlalite_table_name(app): importorskip("flask_sqlalchemy_lite") from flask_sqlalchemy_lite import SQLAlchemy from sqlalchemy.orm import DeclarativeBase from flask_security.models import sqla as sqla from flask_security import FSQLALiteUserDatastore app.config |= { "SQLALCHEMY_ENGINES": { "default": "sqlite:///:memory:", }, } db = SQLAlchemy(app) class Model(DeclarativeBase): pass sqla.FsModels.set_db_info( base_model=Model, user_table_name="myuser", role_table_name="myrole", webauthn_table_name="mywebauthn", ) class Role(Model, sqla.FsRoleMixin): __tablename__ = "myrole" class WebAuthn(Model, sqla.FsWebAuthnMixin): __tablename__ = "mywebauthn" class User(Model, sqla.FsUserMixin): __tablename__ = "myuser" with app.app_context(): Model.metadata.create_all(db.engine) ds = FSQLALiteUserDatastore(db, User, Role, WebAuthn) app.security = Security(app, datastore=ds) with app.app_context(): ds.create_role(name="r1") ds.create_user(email="me@lp.com", roles=["r1"]) ds.commit() user = ds.find_user(email="me@lp.com") assert user with app.app_context(): Model.metadata.drop_all(db.engine) db.engine.dispose() def test_null_fs_uniquifier(app, client): # If a record has a null fs_uniquifier - we shouldn't find it. # The only way this might happen is if an app upgrades from a 3.0 Flask-Security # and doesn't properly update their DB. ds = app.security.datastore with app.test_request_context("/"): user = ds.find_user(email="gal@lp.com") user.fs_uniquifier = "" ds.put(user) ds.commit() user = app.security.login_manager.user_callback("") assert not user def test_refresh_tracker_cleanup(app, datastore): if not datastore.refresh_tracker_model: skip( f"No RefreshTracker model defined for datastore:" f" {datastore.__class__.__name__}" ) init_app_with_options(app, datastore) with app.app_context(): user = datastore.find_user(email="matt@lp.com") expires_at = app.security.datetime_factory() - timedelta(days=1) datastore.create_refresh_tracker( user=user, name="expired", expires_at=expires_at ) expires_at = app.security.datetime_factory() + timedelta(days=1) rt2 = datastore.create_refresh_tracker( user=user, name="revoked", expires_at=expires_at ) datastore.revoke_refresh_tracker(rt2) datastore.commit() assert len(user.refresh_trackers) == 2 datastore.cleanup_refresh_trackers(user, revoked=False) datastore.commit() assert len(user.refresh_trackers) == 1 assert user.refresh_trackers[0].name == "revoked" datastore.cleanup_refresh_trackers(user, revoked=True) datastore.commit() assert len(user.refresh_trackers) == 0 def test_refresh_tracker_cascade(app, datastore): if not datastore.refresh_tracker_model: skip( f"No RefreshTracker model defined for datastore:" f" {datastore.__class__.__name__}" ) init_app_with_options(app, datastore) with app.app_context(): user = datastore.find_user(email="matt@lp.com") expires_at = app.security.datetime_factory() - timedelta(days=1) rt = datastore.create_refresh_tracker( user=user, name="expired", expires_at=expires_at ) rtid = rt.refresh_family datastore.commit() assert len(user.refresh_trackers) == 1 assert datastore.find_refresh_tracker(rtid) # delete user datastore.delete_user(user) datastore.commit() user = datastore.find_user(email="matt@lp.com") assert not user rt = datastore.find_refresh_tracker(rtid) assert not rt pallets-eco-flask-security-72bf929/tests/test_entities.py000066400000000000000000000051071525721743500236500ustar00rootroot00000000000000""" test_entities ~~~~~~~~~~~~~ Entity tests """ import inspect import pytest from sqlalchemy import Column from flask_security import RoleMixin, UserMixin from flask_security.models import fsqla, fsqla_v2 from flask_security.core import AnonymousUser class Role(RoleMixin): def __init__(self, name): self.name = name class User(UserMixin): def __init__(self, roles): self.roles = roles def test_role_mixin_equal(): admin1 = Role("admin") admin2 = Role("admin") assert admin1 == admin2 def test_role_mixin_not_equal(): admin = Role("admin") editor = Role("editor") assert admin != editor def test_user_mixin_has_role_with_string(): admin = Role("admin") editor = Role("editor") user = User([admin, editor]) assert user.has_role("admin") is True assert user.has_role("editor") is True assert user.has_role(admin) is True assert user.has_role(editor) is True def test_anonymous_user_has_no_roles(): user = AnonymousUser() assert not user.has_role("admin") def get_user_attributes(cls): boring = dir(type("dummy", (object,), {})) return [item for item in inspect.getmembers(cls) if item[0] not in boring] @pytest.mark.filterwarnings("ignore::sqlalchemy.exc.SAWarning") def test_fsqla_fields(): # basic test to verify no one modified fsqla after shipping. # Not perfect since not checking relationships etc. v1_user_attrs = { "id", "email", "password", "username", "active", "create_datetime", "update_datetime", "fs_uniquifier", "confirmed_at", "current_login_at", "current_login_ip", "last_login_at", "last_login_ip", "login_count", "tf_phone_number", "tf_primary_method", "tf_totp_secret", } attrs = { a[0] for a in get_user_attributes(fsqla.FsUserMixin) if isinstance(a[1], Column) } assert attrs == v1_user_attrs v2_user_attrs = {"us_totp_secrets", "us_phone_number"} attrs = { a[0] for a in get_user_attributes(fsqla_v2.FsUserMixin) if isinstance(a[1], Column) } assert attrs == v1_user_attrs.union(v2_user_attrs) v1_role_attrs = {"id", "name", "description", "permissions", "update_datetime"} attrs = { a[0] for a in get_user_attributes(fsqla.FsRoleMixin) if isinstance(a[1], Column) } assert attrs == v1_role_attrs attrs = { a[0] for a in get_user_attributes(fsqla_v2.FsRoleMixin) if isinstance(a[1], Column) } assert attrs == v1_role_attrs pallets-eco-flask-security-72bf929/tests/test_hashing.py000066400000000000000000000157231525721743500234520ustar00rootroot00000000000000""" test_hashing ~~~~~~~~~~~~ hashing tests :copyright: (c) 2019-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import timeit import pytest from pytest import raises from tests.test_utils import authenticate, init_app_with_options from passlib.hash import argon2, pbkdf2_sha256, django_pbkdf2_sha256, plaintext from flask_security.core import _get_pwd_context, _get_hashing_context from flask_security.utils import ( hash_password, verify_password, get_hmac, verify_and_update_password, ) def test_verify_password_double_hash_argon2(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "argon2", "SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS": {"argon2__rounds": 1}, "SECURITY_PASSWORD_SALT": "salty", "SECURITY_PASSWORD_SINGLE_HASH": False, }, ) with app.app_context(): hashed_pwd = hash_password("pass") assert verify_password("pass", hashed_pwd) assert "t=1" in hashed_pwd # Verify double hash assert verify_password("pass", argon2.hash(get_hmac("pass"))) def test_verify_password_double_hash_bcrypt(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "bcrypt", "SECURITY_PASSWORD_SALT": "salty", "SECURITY_PASSWORD_SINGLE_HASH": False, }, ) with app.app_context(): hashed_pwd = hash_password("pass") assert verify_password("pass", hashed_pwd) assert hashed_pwd.startswith("$2") def test_verify_password_single_hash(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "argon2", "SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS": {"argon2__rounds": 1}, "SECURITY_PASSWORD_SALT": None, "SECURITY_PASSWORD_SINGLE_HASH": True, }, ) with app.app_context(): assert verify_password("pass", hash_password("pass")) def test_verify_password_single_hash_list(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "argon2", "SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS": {"argon2__rounds": 1}, "SECURITY_PASSWORD_SALT": "salty", "SECURITY_PASSWORD_SINGLE_HASH": ["django_pbkdf2_sha256", "plaintext"], "SECURITY_PASSWORD_SCHEMES": [ "argon2", "pbkdf2_sha256", "django_pbkdf2_sha256", "plaintext", ], }, ) with app.app_context(): # double hash assert verify_password("pass", hash_password("pass")) assert verify_password("pass", pbkdf2_sha256.hash(get_hmac("pass"))) # single hash assert verify_password("pass", django_pbkdf2_sha256.hash("pass")) assert verify_password("pass", plaintext.hash("pass")) def test_verify_password_backward_compatibility(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "argon2", "SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS": {"argon2__rounds": 1}, "SECURITY_PASSWORD_SINGLE_HASH": False, "SECURITY_PASSWORD_SCHEMES": ["argon2", "plaintext"], }, ) with app.app_context(): # double hash assert verify_password("pass", hash_password("pass")) # single hash assert verify_password("pass", plaintext.hash("pass")) def test_login_with_bcrypt_enabled(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "bcrypt", "SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS": { "bcrypt__rounds": 4, # minimum so test is faster }, "SECURITY_PASSWORD_SALT": "salty", "SECURITY_PASSWORD_SINGLE_HASH": False, }, ) response = authenticate(app.test_client(), follow_redirects=True) assert b"Home Page" in response.data # Above created all accounts using bcrypt - now change over to argon # and verify that verify_and_update_password works app.config["SECURITY_PASSWORD_HASH"] = "argon2" app.config["SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS"] = dict(argon2__rounds=1) app.config["SECURITY_PASSWORD_SCHEMES"] = ["argon2", "bcrypt"] app.security.pwd_context = _get_pwd_context(app) app.security.hashing_context = _get_hashing_context(app) with app.app_context(): user1 = sqlalchemy_datastore.find_user(email="gal@lp.com") assert app.security.pwd_context.identify(user1.password) == "bcrypt" assert verify_and_update_password("password", user1) # above should have rewritten password to argon1 sqlalchemy_datastore.commit() assert verify_and_update_password("password", user1) user1 = sqlalchemy_datastore.find_user(email="gal@lp.com") assert app.security.pwd_context.identify(user1.password) == "argon2" def test_missing_hash_salt_option(app, sqlalchemy_datastore): with raises(RuntimeError): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "argon2", "SECURITY_PASSWORD_SALT": None, "SECURITY_PASSWORD_SINGLE_HASH": False, }, ) def test_verify_password_argon2_opts(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "argon2", "SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS": { "argon2__rounds": 1, "argon2__salt_size": 16, "argon2__hash_len": 16, }, }, ) with app.app_context(): hashed_pwd = hash_password("pass") assert "t=1" in hashed_pwd assert verify_password("pass", hashed_pwd) @pytest.mark.skip def test_bcrypt_speed(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "bcrypt", "SECURITY_PASSWORD_SALT": "salty", "SECURITY_PASSWORD_SINGLE_HASH": False, }, ) with app.app_context(): print(timeit.timeit(lambda: hash_password("pass"), number=100)) @pytest.mark.skip def test_argon2_speed(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "argon2", "SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS": {"argon2__rounds": 10}, }, ) with app.app_context(): print( "Hash time for {} iterations: {}".format( 100, timeit.timeit(lambda: hash_password("pass"), number=100) ) ) pallets-eco-flask-security-72bf929/tests/test_misc.py000066400000000000000000001637551525721743500227750ustar00rootroot00000000000000""" test_misc ~~~~~~~~~~~ Lots of tests :copyright: (c) 2012 by Matt Wright. :copyright: (c) 2019-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from datetime import timedelta import hashlib from unittest import mock import re import os.path import time import typing as t import pytest from itsdangerous import BadTimeSignature from wtforms.validators import DataRequired, Length from tests.test_utils import ( authenticate, capture_flashes, capture_reset_password_requests, check_location, check_signals, check_xlation, get_csrf_token, init_app_with_options, json_authenticate, logout, populate_data, reset_fresh, get_form_input, is_authenticated, ) from tests.test_webauthn import HackWebauthnUtil, reg_2_keys from flask import Flask, abort, request, Response from flask_security import Security from flask_security.forms import ( ChangePasswordForm, ConfirmRegisterForm, EmailField, EmailValidation, ForgotPasswordForm, LoginForm, PasswordField, PasswordlessLoginForm, RegisterForm, RegisterFormV2, RequiredLocalize, ResetPasswordForm, SendConfirmationForm, StringField, email_required, valid_user_email, ) from flask_security import auth_required, roles_required from flask_security.utils import ( base_render_json, _encode_string, json_error_response, get_request_attr, hash_data, send_mail, uia_email_mapper, uia_phone_mapper, verify_hash, _get_post_action_redirect, _td_format, ) from flask_security.core import _get_serializer if t.TYPE_CHECKING: # pragma: no cover from flask.testing import FlaskClient @pytest.mark.recoverable() def test_my_mail_util(app, sqlalchemy_datastore): from flask_security import MailUtil class MyMailUtil(MailUtil): def send_mail( self, template, subject, recipient, sender, body, html, user, **kwargs ): assert template == "security/email/reset_instructions" assert subject == app.config["SECURITY_EMAIL_SUBJECT_PASSWORD_RESET"] assert recipient == "matt@lp.com" assert user.email == "matt@lp.com" assert sender == "no-reply@localhost" assert isinstance(sender, str) init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"mail_util_cls": MyMailUtil}} ) client = app.test_client() client.post("/reset", data=dict(email="matt@lp.com")) def test_register_blueprint_flag(app, sqlalchemy_datastore): app.security = Security( app, datastore=sqlalchemy_datastore, register_blueprint=False ) client = app.test_client() response = client.get("/login") assert response.status_code == 404 @pytest.mark.registerable() @pytest.mark.recoverable() @pytest.mark.changeable() @pytest.mark.settings( USER_IDENTITY_ATTRIBUTES=[ {"email": {"mapper": uia_email_mapper}}, {"username": {"mapper": lambda x: x}}, ] ) @pytest.mark.filterwarnings( "ignore:.*The RegisterForm is deprecated.*:DeprecationWarning" ) def test_basic_custom_forms(app, sqlalchemy_datastore): class MyLoginForm(LoginForm): username = StringField("My Login Username Field") class MyRegisterForm(RegisterForm): email = EmailField("My Register Email Address Field") class MyForgotPasswordForm(ForgotPasswordForm): email = EmailField( "My Forgot Email Address Field", validators=[email_required, EmailValidation(verify=True), valid_user_email], ) class MyResetPasswordForm(ResetPasswordForm): password = StringField("My Reset Password Field") class MyChangePasswordForm(ChangePasswordForm): password = PasswordField("My Change Password Field") app.security = Security( app, datastore=sqlalchemy_datastore, login_form=MyLoginForm, register_form=MyRegisterForm, forgot_password_form=MyForgotPasswordForm, reset_password_form=MyResetPasswordForm, change_password_form=MyChangePasswordForm, ) populate_data(app) client = app.test_client() response = client.get("/login") assert b"My Login Username Field" in response.data response = client.get("/register") assert b"My Register Email Address Field" in response.data response = client.get("/reset") assert b"My Forgot Email Address Field" in response.data with capture_reset_password_requests() as requests: response = client.post("/reset", data=dict(email="matt@lp.com")) token = requests[0]["token"] response = client.get("/reset/" + token) assert b"My Reset Password Field" in response.data authenticate(client) response = client.get("/change") assert b"My Change Password Field" in response.data @pytest.mark.registerable() @pytest.mark.confirmable() @pytest.mark.settings(use_register_v2=False) @pytest.mark.filterwarnings("ignore:.*The ConfirmRegisterForm.*:DeprecationWarning") def test_confirmable_custom_form(app, sqlalchemy_datastore): class MyRegisterForm(ConfirmRegisterForm): email = EmailField("My Register Email Address Field") class MySendConfirmationForm(SendConfirmationForm): email = EmailField("My Send Confirmation Email Address Field") app.security = Security( app, datastore=sqlalchemy_datastore, send_confirmation_form=MySendConfirmationForm, confirm_register_form=MyRegisterForm, ) client = app.test_client() response = client.get("/register") assert b"My Register Email Address Field" in response.data response = client.get("/confirm") assert b"My Send Confirmation Email Address Field" in response.data def test_passwordless_custom_form(app, sqlalchemy_datastore): app.config["SECURITY_PASSWORDLESS"] = True class MyPasswordlessLoginForm(PasswordlessLoginForm): email = EmailField("My Passwordless Email Address Field") app.security = Security( app, datastore=sqlalchemy_datastore, passwordless_login_form=MyPasswordlessLoginForm, ) client = app.test_client() response = client.get("/login") assert b"My Passwordless Email Address Field" in response.data @pytest.mark.parametrize("logout_methods", (["GET", "POST"], ["GET"], ["POST"])) def test_logout_methods(app, sqlalchemy_datastore, logout_methods): init_app_with_options( app, sqlalchemy_datastore, **{"SECURITY_LOGOUT_METHODS": logout_methods} ) client = app.test_client() authenticate(client) response = client.get("/logout", follow_redirects=True) if "GET" in logout_methods: assert response.status_code == 200 authenticate(client) else: assert response.status_code == 405 # method not allowed response = client.post("/logout", follow_redirects=True) if "POST" in logout_methods: assert response.status_code == 200 else: assert response.status_code == 405 # method not allowed def test_logout_methods_none(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{"SECURITY_LOGOUT_METHODS": None} ) client = app.test_client() authenticate(client) response = client.get("/logout", follow_redirects=True) assert response.status_code == 404 response = client.post("/logout", follow_redirects=True) assert response.status_code == 404 def test_passwordless_and_two_factor_configuration_mismatch(app, sqlalchemy_datastore): with pytest.raises(ValueError): init_app_with_options( app, sqlalchemy_datastore, **{"SECURITY_TWO_FACTOR": True, "SECURITY_TWO_FACTOR_ENABLED_METHODS": []}, ) def test_flash_messages_off(app, sqlalchemy_datastore, get_message): init_app_with_options( app, sqlalchemy_datastore, **{"SECURITY_FLASH_MESSAGES": False} ) client = app.test_client() response = client.get("/profile") assert get_message("LOGIN") not in response.data def test_invalid_hash_scheme(app, sqlalchemy_datastore, get_message): with pytest.raises(ValueError): init_app_with_options( app, sqlalchemy_datastore, **{"SECURITY_PASSWORD_HASH": "bogus"} ) def test_change_hash_type(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "SECURITY_PASSWORD_HASH": "plaintext", "SECURITY_PASSWORD_SALT": None, "SECURITY_PASSWORD_SCHEMES": ["argon2", "plaintext"], }, ) app.config["SECURITY_PASSWORD_HASH"] = "argon2" app.config["SECURITY_PASSWORD_SALT"] = "salty" app.security = Security( app, datastore=sqlalchemy_datastore, register_blueprint=False ) client = app.test_client() response = client.post( "/login", data=dict(email="matt@lp.com", password="password") ) assert response.status_code in [302, 303] response = client.get("/logout") response = client.post( "/login", data=dict(email="matt@lp.com", password="password") ) assert response.status_code in [302, 303] @pytest.mark.settings(hashing_schemes=["hex_md5"], deprecated_hashing_schemes=[]) @pytest.mark.parametrize("data", ["hellö", b"hello"]) def test_legacy_hash(in_app_context, data): legacy_hash = hashlib.md5(_encode_string(data)).hexdigest() new_hash = hash_data(data) assert legacy_hash == new_hash def test_hash_data(in_app_context): data = hash_data(b"hello") assert isinstance(data, str) data = hash_data("hellö") assert isinstance(data, str) def test_verify_hash(in_app_context): data = hash_data("hellö") assert verify_hash(data, "hellö") is True assert verify_hash(data, "hello") is False legacy_data = hashlib.md5(_encode_string("hellö")).hexdigest() assert verify_hash(legacy_data, "hellö") is True assert verify_hash(legacy_data, "hello") is False @pytest.mark.settings( password_salt="öööööööööööööööööööööööööööööööööö", password_hash="bcrypt" ) def test_password_unicode_password_salt(client): response = authenticate(client) assert response.status_code in [302, 303] response = authenticate(client, follow_redirects=True) assert b"Welcome matt@lp.com" in response.data @pytest.mark.registerable() def test_custom_forms_via_config(app, sqlalchemy_datastore): class MyLoginForm(LoginForm): email = StringField("My Login Email Address Field") class MyRegisterForm(RegisterFormV2): email = StringField("My Register Email Address Field") app.config["SECURITY_LOGIN_FORM"] = MyLoginForm app.config["SECURITY_REGISTER_FORM"] = MyRegisterForm security = Security(datastore=sqlalchemy_datastore) security.init_app(app) client = app.test_client() response = client.get("/login") assert b"My Login Email Address Field" in response.data response = client.get("/register") assert b"My Register Email Address Field" in response.data def test_custom_form_instantiator(app, client, get_message): # Test application form instantiation. # This is using the form factory pattern. # Note in this case - Flask-Security doesn't even know the form class name. from flask_security import FormInfo class FormInstantiator: def __init__(self, myservice): self.myservice = myservice def instantiator(self, form_name, form_cls, *args, **kwargs): if form_name == "login_form": return MyLoginForm(*args, service=self.myservice, **kwargs) raise ValueError("Unknown Form") class MyLoginForm(LoginForm): def __init__(self, *args, service=None, **kwargs): super().__init__(*args, **kwargs) self.myservice = service def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): # pragma: no cover return False assert isinstance(self.email.errors, list) if not self.myservice(self.email.data): self.email.errors.append("Not happening") return False return True def login_checker(email): return True if email == "matt@lp.com" else False fi = FormInstantiator(login_checker) app.security.set_form_info("login_form", FormInfo(fi.instantiator)) response = authenticate(client, follow_redirects=True) assert b"Welcome matt@lp.com" in response.data logout(client) # Try a normally legit user - but our service denies it response = authenticate(client, email="joe@lp.com") assert b"Not happening" in response.data def test_custom_form_instantiator2(app, client, get_message): # Test application form instantiation. # This is using the form clone pattern. # Note in this case - Flask-Security doesn't even know the form class name. app.config["WTF_CSRF_ENABLED"] = True from flask_security import FormInfo class MyLoginForm(LoginForm): def __init__(self, *args, service=None, **kwargs): super().__init__(*args, **kwargs) self.myservice = service def instantiator(self, form_name, form_cls, *args, **kwargs): return MyLoginForm(*args, service=self.myservice, **kwargs) def validate(self, **kwargs: t.Any) -> bool: if not super().validate(**kwargs): # pragma: no cover return False assert isinstance(self.email.errors, list) if not self.myservice(self.email.data): self.email.errors.append("Not happening") return False return True def login_checker(email): return True if email == "matt@lp.com" else False with app.test_request_context(): fi = MyLoginForm(formdata=None, service=login_checker) app.security.set_form_info("login_form", FormInfo(fi.instantiator)) csrf_token = get_csrf_token(client) response = client.post( "/login", data=dict(email="matt@lp.com", password="password", csrf_token=csrf_token), follow_redirects=True, ) assert b"Welcome matt@lp.com" in response.data logout(client) # Try a normally legit user - but our service denies it csrf_token = get_csrf_token(client) response = client.post( "/login", data=dict(email="joe@lp.com", password="password", csrf_token=csrf_token), ) assert b"Not happening" in response.data def test_custom_form_setting(app, sqlalchemy_datastore): from flask_security import FormInfo security = Security(app=app, datastore=sqlalchemy_datastore) with pytest.raises(ValueError) as vex: security.set_form_info("mylogin_form", FormInfo()) assert "Unknown form name mylogin_form" == str(vex.value) with pytest.raises(ValueError) as vex: security.set_form_info("login_form", FormInfo()) assert "form class must be provided" in str(vex.value) def test_form_required(app, sqlalchemy_datastore): class MyLoginForm(LoginForm): myfield = StringField("My Custom Field", validators=[RequiredLocalize()]) app.config["SECURITY_LOGIN_FORM"] = MyLoginForm security = Security(datastore=sqlalchemy_datastore) security.init_app(app) client = app.test_client() response = client.post("/login", content_type="application/json") assert response.status_code == 400 assert b"myfield" in response.data def test_form_required_local_message(app, sqlalchemy_datastore): """Test having a local message (not xlatable and not part of MSG_ config.""" msg = "hi! did you forget me?" class MyLoginForm(LoginForm): myfield = StringField( "My Custom Field", validators=[RequiredLocalize(message=msg)] ) app.config["SECURITY_LOGIN_FORM"] = MyLoginForm security = Security(datastore=sqlalchemy_datastore) security.init_app(app) client = app.test_client() response = client.post("/login", content_type="application/json") assert response.status_code == 400 assert b"myfield" in response.data assert msg.encode("utf-8") in response.data # WTforms 2.x incorrectly catches ValueError and sets that as the form error. # Our config_value routine raises ValueError for missing config items.. assert b"Key" not in response.data def test_without_babel(app, client): # Test if babel modules exist but we don't init babel - things still work app.config["BABEL_DEFAULT_LOCALE"] = "fr_FR" response = client.get("/login") assert response.status_code == 200 def test_no_email_sender(app, sqlalchemy_datastore, outbox): """Verify that if SECURITY_EMAIL_SENDER is default (which is a local proxy) that send_mail picks up MAIL_DEFAULT_SENDER. """ app.config["MAIL_DEFAULT_SENDER"] = "test@testme.com" class TestUser: def __init__(self, email): self.email = email security = Security() security.init_app(app, sqlalchemy_datastore) with app.app_context(): user = TestUser("matt@lp.com") send_mail( "Test Default Sender", user.email, "security/email/welcome", user=user ) assert 1 == len(outbox) assert "test@testme.com" == outbox[0].sender def test_sender_tuple(app, sqlalchemy_datastore, outbox): """Verify that if sender is a (name, address) tuple, in the received email sender is properly formatted as "name
    " Flask-Mail takes tuples - Flask-Mailman takes them - however the local-mem backend doesn't format them correctly (SMTP backend doesn't work either?) """ app.config["MAIL_DEFAULT_SENDER"] = ("Test User", "test@testme.com") class TestUser: def __init__(self, email): self.email = email security = Security() security.init_app(app, sqlalchemy_datastore) with app.app_context(): user = TestUser("matt@lp.com") send_mail("Test Tuple Sender", user.email, "security/email/welcome", user=user) assert 1 == len(outbox) assert outbox[0].sender == "Test User " def test_send_mail_context(app, sqlalchemy_datastore, outbox): """Test full context sent to MailUtil/send_mail""" app.config["MAIL_DEFAULT_SENDER"] = "test@testme.com" app.security = Security() app.security.init_app(app, sqlalchemy_datastore) class TestUser: def __init__(self, email): self.email = email @app.security.mail_context_processor def mail(): return {"foo": "bar-mail"} with app.app_context(): user = TestUser("matt@lp.com") send_mail( "Test Default Sender", user.email, "security/email/welcome", user=user ) assert 1 == len(outbox) assert "test@testme.com" == outbox[0].sender matcher = re.match( r".*ExtraContext:(\S+).*", outbox[0].body, re.IGNORECASE | re.DOTALL ) assert matcher.group(1) == "bar-mail" @pytest.mark.babel(babel_default_locale="cic_US", test_xlations=True) def test_xlation(app, client): response = client.get("/login") assert b'' in response.data response = authenticate(client) assert response.status_code in [302, 303] response = authenticate(client, follow_redirects=True) assert b"Welcome OKAY! matt@lp.com" in response.data @pytest.mark.babel() @pytest.mark.app_settings(babel_default_locale="fr_FR") def test_myxlation(app, sqlalchemy_datastore, pytestconfig): # Test changing a single MSG and having an additional translation dir # Flask-BabelEx doesn't support lists of directories.. pytest.importorskip("flask_babel") i18n_dirname = [ "builtin", os.path.join(pytestconfig.rootdir, "tests/translations"), ] init_app_with_options( app, sqlalchemy_datastore, **{"SECURITY_I18N_DIRNAME": i18n_dirname} ) assert check_xlation(app, "fr_FR"), "You must run python setup.py compile_catalog" app.config["SECURITY_MSG_INVALID_PASSWORD"] = ("Password no-worky", "error") client = app.test_client() response = client.post("/login", data=dict(email="matt@lp.com", password="forgot")) assert b"Passe - no-worky" in response.data @pytest.mark.babel(babel_default_locale="fr_FR", test_xlations=True) def test_myxlation_complete(app, client): # Test having own translations and not using builtin app.config["SECURITY_MSG_INVALID_PASSWORD"] = ("Password no-worky", "error") response = client.post("/login", data=dict(email="matt@lp.com", password="forgot")) assert b"Passe - no-worky" in response.data @pytest.mark.babel(babel_default_locale="cic_US", test_xlations=True) def test_isstring_xlation(app, client): # Test that IsString's error message is properly localized. response = client.post( "/login", json=dict(email={"not": "a string"}, password="password") ) assert response.status_code == 400 assert "OKAY!" in response.json["response"]["errors"][0] @pytest.mark.babel(babel_default_locale="cic_US", test_xlations=True) def test_form_labels(app, sqlalchemy_datastore): app.security = Security() app.security.init_app(app, sqlalchemy_datastore) with app.test_request_context(): form = RegisterForm() for field in ["password", "password_confirm", "email", "submit"]: assert "OKAY!" in str(getattr(form, field).label.text) form = LoginForm() for field in ["password", "remember", "email", "submit"]: assert "OKAY!" in str(getattr(form, field).label.text) form = ChangePasswordForm() for field in ["password", "new_password", "new_password_confirm", "submit"]: assert "OKAY!" in str(getattr(form, field).label.text) @pytest.mark.babel(babel_default_locale="fr_FR", test_xlations=True) def test_wtform_xlation(app, sqlalchemy_datastore): # Make sure wtform xlations work class MyLoginForm(LoginForm): fixed_length = StringField( "FixedLength", validators=[DataRequired(), Length(3, 3)] ) app.security = Security() app.security.init_app(app, datastore=sqlalchemy_datastore, login_form=MyLoginForm) client = app.test_client() response = client.get("/login") assert b'' in response.data data = dict( email="matt@lp.com", password="", remember="y", fixed_length="waytoolong" ) response = client.post( "/login", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 flerror = response.json["response"]["field_errors"]["fixed_length"][0] # This is completely dependent on WTforms translations.... assert ( flerror == "Le doit contenir exactement 3 caractères." or flerror == "Le champ doit contenir exactement 3 caractères." ) @pytest.mark.changeable() @pytest.mark.babel(test_xlations=True) def test_per_request_xlate(app, client): from flask import request, session babel = app.extensions["babel"] def get_locale(): # For a given session - set lang based on first request. # Honor explicit url request first if "lang" not in session: locale = request.args.get("lang", None) if not locale: locale = request.accept_languages.best if locale: session["lang"] = locale return session.get("lang", None).replace("-", "_") babel.locale_selector_func = get_locale babel.locale_selector = get_locale # Flask-Babel >= 3.0.0 response = client.get("/login", headers=[("Accept-Language", "cic_US")]) assert b'' in response.data # make sure template contents get xlated (not just form). assert b"

    Login OKAY!

    " in response.data data = dict(email="matt@lp.com", password="", remember="y") response = client.post("/login", data=data, headers=[("Accept-Language", "cic_US")]) assert response.status_code == 200 # verify errors are xlated assert b"Password not provided OKAY!" in response.data # log in correctly - this should set locale in session data = dict(email="matt@lp.com", password="password", remember="y") response = client.post( "/login", data=data, headers=[("Accept-Language", "cic_US")], follow_redirects=True, ) assert response.status_code == 200 # make sure further requests always get correct xlation w/o sending header response = client.get("/change", follow_redirects=True) assert response.status_code == 200 assert b"Change Password OKAY!" in response.data assert b"

    Change Password OKAY!

    " in response.data # try JSON response = client.post( "/change", json=dict(email="matt@lp.com"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 400 assert response.json["response"]["field_errors"]["new_password"] == [ "Password not provided OKAY!" ] """ This cant work yet due to zxcvbn usage of gettext def test_zxcvbn_xlate(app): class TestUser(object): def __init__(self, email): self.email = email app.config["BABEL_DEFAULT_LOCALE"] = "fr_FR" app.security = Security() app.security.init_app(app) assert check_xlation(app, "fr_FR"), "You must run python setup.py compile_catalog" with app.test_request_context(): user = TestUser("jwag@notme.com") pbad, pnorm = app.security.password_util.validate("simple", False, user=user) print(pbad) """ @pytest.mark.settings(password_check_breached="strict") def test_breached(app, sqlalchemy_datastore): # partial response from: https://api.pwnedpasswords.com/range/07003 pwned_response = b"AF5A73CD3CBCFDCD12B0B68CB7930F3E888:2\r\n\ AFD8AA47E6FD782ADDC11D89744769F7354:2\r\n\ B04334E179537C975D0B3C72DA2E5B68E44:15\r\n\ B118F58C2373FDF97ACF93BD3339684D1EB:2\r\n\ B1ED5D27429EDF77EFD84F4EA9BDA5013FB:4\r\n\ B25C03CFBE4CBF19E0F4889711C9A488E5D:2\r\n\ B3902FD808DCA504AAAD30F3C14BD3ACE7C:10" app.security = Security() app.security.init_app(app, sqlalchemy_datastore) with app.test_request_context(): with mock.patch("urllib.request.urlopen") as mock_urlopen: mock_urlopen.return_value.__enter__.return_value.read.return_value = ( pwned_response ) pbad, pnorm = app.security.password_util.validate("flaskflask", False) assert len(pbad) == 1 assert app.config["SECURITY_MSG_PASSWORD_BREACHED"][0] in pbad[0] @pytest.mark.settings( password_check_breached="strict", password_breached_count=16, password_complexity_checker="zxcvbn", ) def test_breached_cnt(app, sqlalchemy_datastore): # partial response from: https://api.pwnedpasswords.com/range/07003 pwned_response = b"AF5A73CD3CBCFDCD12B0B68CB7930F3E888:2\r\n\ AFD8AA47E6FD782ADDC11D89744769F7354:2\r\n\ B04334E179537C975D0B3C72DA2E5B68E44:15\r\n\ B118F58C2373FDF97ACF93BD3339684D1EB:2\r\n\ B1ED5D27429EDF77EFD84F4EA9BDA5013FB:4\r\n\ B25C03CFBE4CBF19E0F4889711C9A488E5D:2\r\n\ B3902FD808DCA504AAAD30F3C14BD3ACE7C:10" app.security = Security() app.security.init_app(app, sqlalchemy_datastore) with app.test_request_context(): with mock.patch("urllib.request.urlopen") as mock_urlopen: mock_urlopen.return_value.__enter__.return_value.read.return_value = ( pwned_response ) pbad, pnorm = app.security.password_util.validate("flaskflask", True) # Still weak password, just not pwned enough. Should fail complexity assert len(pbad) == 1 assert "Repeats like" in pbad[0] @pytest.mark.skip @pytest.mark.settings(password_check_breached="strict") def test_breached_real(app, sqlalchemy_datastore): """Actually go out to internet..""" app.security = Security() app.security.init_app(app, sqlalchemy_datastore) with app.test_request_context(): pbad, pnorm = app.security.password_util.validate("flaskflask", True) assert len(pbad) == 1 assert app.config["SECURITY_MSG_PASSWORD_BREACHED"][0] in pbad[0] def test_json_error_response_string(): """Unit test for correct response when a string is given.""" error_msg = "This is an error!" response = json_error_response(errors=error_msg) assert "field_errors" not in response assert response["errors"][0] == error_msg def test_json_error_response_dict(): """Unit test for correct response when a dict is given.""" error_msg = { "e-mail": ["The e-mail address is already in the system."], "name": ["The name is too long.", "Nice name"], } all_msgs = [] [all_msgs.extend(m) for m in error_msg.values()] response = json_error_response(field_errors=error_msg) assert "errors" in response assert "field_errors" in response assert all(m in response["errors"] for m in all_msgs) def test_json_error_response_typeerror(): """Unit test for checking for error raising.""" error_msg = ("tuple",) with pytest.raises(TypeError): json_error_response(errors=error_msg) def test_json_form_errors(app, client): """Test wtforms form level errors are correctly sent via json""" with app.test_request_context(): form = ChangePasswordForm() form.validate() form.form_errors.append("I am an error") response = base_render_json(form) error_list = response.json["response"]["errors"] assert len(error_list) == 3 assert "I am an error" in error_list def test_method_view(app, client): # auth_required with flask method view from flask.views import MethodView from flask import render_template_string class MyView(MethodView): decorators = [auth_required("token", "session")] def get(self): return render_template_string("Hi view") myview = MyView.as_view("myview") app.add_url_rule("/myview", view_func=myview, methods=["GET"]) response = client.get("/myview", follow_redirects=False) # should require login assert response.status_code in [302, 303] assert "/login" in response.location authenticate(client) response = client.get("/myview") assert response.status_code == 200 assert b"Hi view" in response.data def test_phone_util_override(app, sqlalchemy_datastore): from flask_security import phone_util class MyPhoneUtil(phone_util.PhoneUtil): def validate_phone_number(self, input_data): return "call-me" def get_canonical_form(self, input_data): return "very-canonical" app.security = Security(phone_util_cls=MyPhoneUtil) app.security.init_app(app, sqlalchemy_datastore) with app.app_context(): assert uia_phone_mapper("55") == "very-canonical" def test_authn_freshness( app: "Flask", client: "FlaskClient", get_message: t.Callable[..., bytes] ) -> None: """Test freshness using default reauthn_handler""" @auth_required(within=30, grace=0) def myview(): return Response(status=200) @auth_required(within=0.001, grace=0) def myspecialview(): return Response(status=200) app.add_url_rule("/myview", view_func=myview, methods=["GET"]) app.add_url_rule("/myspecialview", view_func=myspecialview, methods=["GET"]) authenticate(client) # This should work and not be redirected response = client.get("/myview", follow_redirects=False) assert response.status_code == 200 # This should require additional authn and redirect to verify time.sleep(0.1) with capture_flashes() as flashes: response = client.get("/myspecialview", follow_redirects=False) assert response.status_code in [302, 303] assert response.location == "/verify?next=/myspecialview" assert flashes[0]["category"] == "error" assert flashes[0]["message"].encode("utf-8") == get_message( "REAUTHENTICATION_REQUIRED" ) # Test json error response response = client.get("/myspecialview", headers={"accept": "application/json"}) assert response.status_code == 401 assert response.json and response.json["response"]["errors"][0].encode( "utf-8" ) == get_message("REAUTHENTICATION_REQUIRED") def test_authn_freshness_handler(app, client, get_message): """Test with our own handler""" @app.security.reauthn_handler def my_reauthn(within, grace, headers=None): assert within == timedelta(minutes=30) or timedelta(minutes=0.001) if app.security._want_json(request): payload = json_error_response(errors="Oh No") return app.security._render_json(payload, 401, headers, None) abort(500) @auth_required(within=30, grace=0) def myview(): return Response(status=200) @auth_required(within=0.001, grace=0) def myspecialview(): return Response(status=200) app.add_url_rule("/myview", view_func=myview, methods=["GET"]) app.add_url_rule("/myspecialview", view_func=myspecialview, methods=["GET"]) authenticate(client) # This should work and not be redirected response = client.get("/myview", follow_redirects=False) assert response.status_code == 200 # This should require additional authn time.sleep(0.1) response = client.get("/myspecialview", follow_redirects=False) assert response.status_code == 500 # Test json error response response = client.get("/myspecialview", headers={"accept": "application/json"}) assert response.status_code == 401 assert response.json["response"]["errors"][0] == "Oh No" def test_authn_freshness_callable(app, client, get_message): @auth_required(within=lambda: timedelta(minutes=30)) def myview(): return Response(status=200) app.add_url_rule("/myview", view_func=myview, methods=["GET"]) authenticate(client) # This should work and not be redirected response = client.get("/myview", follow_redirects=False) assert response.status_code == 200 @pytest.mark.settings(url_prefix="/myprefix") def test_default_authn_bp(app, client): """Test default reauthn handler with blueprint prefix""" @auth_required(within=1, grace=0) def myview(): return Response(status=200) app.add_url_rule("/myview", view_func=myview, methods=["GET"]) authenticate(client, endpoint="/myprefix/login") # This should require additional authn and redirect to verify reset_fresh(client, within=timedelta(minutes=1)) response = client.get("/myview", follow_redirects=False) assert response.status_code in [302, 303] assert response.location == "/myprefix/verify?next=/myview" def test_authn_freshness_grace(app, client, get_message): # Test that grace override within. @auth_required(within=lambda: timedelta(minutes=30), grace=10) def myview(): return Response(status=200) @auth_required(within=0.001, grace=lambda: timedelta(minutes=10)) def myspecialview(): return Response(status=200) app.add_url_rule("/myview", view_func=myview, methods=["GET"]) app.add_url_rule("/myspecialview", view_func=myspecialview, methods=["GET"]) authenticate(client) # This should work and not be redirected response = client.get("/myview", follow_redirects=False) assert response.status_code == 200 # This should NOT require additional authn time.sleep(0.1) response = client.get("/myspecialview", follow_redirects=False) assert response.status_code == 200 def test_authn_freshness_nc(app, client_nc, get_message): # By default, auth token carries the fs_paa time. @auth_required(within=30) def myview(): return Response(status=200) app.add_url_rule("/myview", view_func=myview, methods=["GET"]) response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] h = {"Authentication-Token": token} # This should fail - should be a redirect response = client_nc.get("/myview", headers=h, follow_redirects=False) assert response.status_code == 200 @pytest.mark.settings(freshness_allow_auth_token=False) def test_authn_freshness_nc_no(app, client_nc, get_message): # If don't send session cookie - then freshness always fails @auth_required(within=30) def myview(): return Response(status=200) app.add_url_rule("/myview", view_func=myview, methods=["GET"]) response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] h = {"Authentication-Token": token} # This should fail - should be a redirect response = client_nc.get("/myview", headers=h, follow_redirects=False) assert response.status_code in [302, 303] assert response.location == "/verify?next=/myview" @pytest.mark.two_factor() @pytest.mark.settings(freshness=timedelta(minutes=-1), multi_factor_recovery_codes=True) def test_authn_freshness_nc_no_fresh(app, client_nc, get_message): # test disabling freshness when no session is sent. response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] h = {"Authentication-Token": token} # This should work response = client_nc.get("/tf-setup", json={}, headers=h) assert response.status_code == 200 assert not response.json["response"]["tf_required"] response = client_nc.get("/mf-recovery-codes", json={}, headers=h) assert response.status_code == 200 assert response.json["response"]["recovery_codes"] == [] def test_verify_fresh(app, client, get_message): # Hit a fresh-required endpoint and walk through verify authenticate(client) reset_fresh(client, app.config["SECURITY_FRESHNESS"]) with capture_flashes() as flashes: response = client.get("/fresh", follow_redirects=True) assert b"Reauthenticate" in response.data assert flashes[0]["category"] == "error" assert flashes[0]["message"].encode("utf-8") == get_message( "REAUTHENTICATION_REQUIRED" ) form_response = response.data.decode("utf-8") matcher = re.match( r'.*action="([^"]*)".*', form_response, re.IGNORECASE | re.DOTALL ) verify_url = matcher.group(1) reset_fresh(client, app.config["SECURITY_FRESHNESS"]) response = client.get(verify_url) assert b"Reauthenticate" in response.data response = client.post( verify_url, data=dict(password="not my password"), follow_redirects=False ) assert b"Reauthenticate" in response.data response = client.post( verify_url, data=dict(password="password"), follow_redirects=False ) assert check_location(app, response.location, "/fresh") # should be fine now response = client.get("/fresh", follow_redirects=True) assert b"Fresh Only" in response.data def test_verify_fresh_json(app, client, get_message, signals): # Hit a fresh-required endpoint and walk through verify authenticate(client) headers = {"Accept": "application/json", "Content-Type": "application/json"} reset_fresh(client, app.config["SECURITY_FRESHNESS"]) response = client.get("/fresh", headers=headers) assert response.status_code == 401 assert response.json["response"]["reauth_required"] assert not response.json["response"]["oauth_enabled"] assert not response.json["response"]["oauth_providers"] response = client.get("/verify") assert b"Reauthenticate" in response.data response = client.post( "/verify", json=dict(password="not my password"), headers=headers ) assert response.status_code == 400 assert signals["user_failed_authn"][0]["request_endpoint"] == "security.verify" assert signals["user_failed_authn"][0]["user"].email == "matt@lp.com" assert signals["user_failed_authn"][0]["auth_type"] == "password" assert not signals["user_failed_authn"][0]["tfa"] response = client.post("/verify", json=dict(password="password"), headers=headers) assert response.status_code == 200 # should be fine now response = client.get("/fresh", headers=headers) assert response.status_code == 200 assert response.json["title"] == "Fresh Only" check_signals(signals, "user_failed_authn") @pytest.mark.changeable() def test_verify_pwd_json(app, client, get_message, signals): # Make sure verify accepts a normalized and original password. authenticate(client) headers = {"Accept": "application/json", "Content-Type": "application/json"} data = dict( password="password", new_password="new strong password\N{ROMAN NUMERAL ONE}", new_password_confirm="new strong password\N{ROMAN NUMERAL ONE}", ) response = client.post( "/change", json=data, headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 response = client.post( "/verify", json=dict(password="new strong password\N{ROMAN NUMERAL ONE}"), headers=headers, ) assert response.status_code == 200 response = client.post( "/verify", json=dict(password="new strong password\N{LATIN CAPITAL LETTER I}"), headers=headers, ) assert response.status_code == 200 check_signals(signals, "password_changed") @pytest.mark.settings(verify_url="/auth/") def test_verify_next(app, client, get_message): authenticate(client) response = client.post( "/auth/?next=http://localhost/mynext", data=dict(password="password"), follow_redirects=False, ) assert response.location == "http://localhost/mynext" # form.next (no query param) response = client.post( "/auth/", data=dict(password="password", next="http://localhost/formnext"), follow_redirects=False, ) assert response.location == "http://localhost/formnext" # GET should copy ?next into the hidden form field response = client.get("/auth/?next=/profile") assert b'name="next"' in response.data assert b'value="/profile"' in response.data # invalid form.next is rejected with INVALID_REDIRECT response = client.post( "/auth/", data=dict(password="password", next="http://evil.example/phish"), follow_redirects=False, ) assert response.status_code == 200 assert get_message("INVALID_REDIRECT") in response.data @pytest.mark.webauthn(webauthn_util_cls=HackWebauthnUtil) def test_verify_wan(app, client, get_message): # test get correct options when requiring a reauthentication and have wan keys # setup. headers = {"Accept": "application/json", "Content-Type": "application/json"} reg_2_keys(client) reset_fresh(client, app.config["SECURITY_FRESHNESS"]) response = client.get("/fresh", headers=headers) assert response.status_code == 401 assert response.json["response"]["reauth_required"] assert response.json["response"]["has_webauthn_verify_credential"] # the verify form should have the webauthn verify form attached response = client.get("verify") assert b'action="/wan-verify"' in response.data app.config["SECURITY_WAN_ALLOW_AS_VERIFY"] = None response = client.get("/fresh", headers=headers) assert response.status_code == 401 assert response.json["response"]["reauth_required"] assert not response.json["response"]["has_webauthn_verify_credential"] # the verify form should NOT have the webauthn verify form attached response = client.get("verify") assert b'action="/wan-verify"' not in response.data def test_direct_decorator(app, client, get_message): """Test/show calling the auth_required decorator directly""" headers = {"Accept": "application/json", "Content-Type": "application/json"} def myview(): return roles_required("author")(domyview)() def domyview(): return Response(status=200) app.add_url_rule("/myview", view_func=myview, methods=["GET"]) authenticate(client) response = client.get("/myview", headers=headers) assert response.status_code == 403 logout(client) authenticate(client, email="jill@lp.com") response = client.get("/myview", headers=headers) assert response.status_code == 200 def test_authn_via(app, client, get_message): """Test that we get correct fs_authn_via set in request""" @auth_required(within=30, grace=0) def myview(): assert get_request_attr("fs_authn_via") == "session" return Response(status=200) app.add_url_rule("/myview", view_func=myview, methods=["GET"]) authenticate(client) # This should work and not be redirected response = client.get("/myview", follow_redirects=False) assert response.status_code == 200 def test_post_security_with_application_root(app, sqlalchemy_datastore): init_app_with_options(app, sqlalchemy_datastore, **{"APPLICATION_ROOT": "/root"}) client = app.test_client() response = client.post( "/login", data=dict(email="matt@lp.com", password="password") ) assert response.status_code in [302, 303] assert "/root" in response.location response = client.post("/logout") assert response.status_code in [302, 303] assert "/root" in response.location def test_post_security_with_application_root_and_views(app, sqlalchemy_datastore): init_app_with_options( app, sqlalchemy_datastore, **{ "APPLICATION_ROOT": "/root", "SECURITY_POST_LOGIN_VIEW": "/post_login", "SECURITY_POST_LOGOUT_VIEW": "/post_logout", }, ) client = app.test_client() response = client.post( "/login", data=dict(email="matt@lp.com", password="password") ) assert response.status_code in [302, 303] assert "/post_login" in response.location response = client.post("/logout") assert response.status_code in [302, 303] assert "/post_logout" in response.location def test_open_redirect(app, client, get_message): """ Test various possible URLs that urlsplit() shows as relative but many browsers will interpret as absolute - and thus have an open-redirect vulnerability. """ test_urls = [ ("\\\\\\github.com", "%5C%5C%5Cgithub.com"), (" //github.com", "%20//github.com"), (r"/\github.com", "/%5Cgithub.com"), (r"\/github.com", "%5C/github.com"), ("//github.com", ""), ("\t//github.com", "%09//github.com"), ("////github.com", ""), ("myhttp://github.com", False), ("http:////github.com", False), (r"\\\localhost", False), (r"\\\\localhost", False), (r"https://localhost\%40evil.com/", False), ("https://localhost%5c%40evil.com/", False), (r"https://localhost\@evil.com/", False), (r"https://localhost@evil.com/", False), (r"https://localhost:localhost@evil.com/", False), ] for nextloc in ["form", "query"]: for i, o in test_urls: if nextloc == "form": data = dict(email="matt@lp.com", password="password", next=i) response = client.post("/login", data=data, follow_redirects=False) else: data = dict(email="matt@lp.com", password="password") response = client.post( f"/login?next={i}", data=data, follow_redirects=False ) if response.status_code in [302, 303]: # this means it passed form validation but should have been quoted assert check_location(app, response.location, o) elif response.status_code == 200: # should have failed form validation assert get_message("INVALID_REDIRECT") in response.data else: raise AssertionError("Bad response code") logout(client) @pytest.mark.settings(redirect_allow_subdomains=True) def test_open_redirect_subdomain(app, client, get_message): # As above - netloc is also susceptible to crazy escaping # The first URL below, if cut-n-pasted into Chrome will end up at amazon.com # The difference in response between in-form and query string is that # the query string is unescaped by Werkzeug but the form value isn't. app.config["SERVER_NAME"] = "lp.com" test_urls = [ ("https://amazon.com\\.lp.com", "https://amazon.com%5C.lp.com", None), ( "https://amazon.com%5C.lp.com", "https://amazon.com%255C.lp.com", "https://amazon.com%5C.lp.com", ), ( "https://jwag:mypass@amazon.com\\.lp.com", "https://jwag:mypass@amazon.com%5C.lp.com", None, ), ] for nextloc in ["form", "query"]: for i, o, oq in test_urls: if nextloc == "form": data = dict(email="matt@lp.com", password="password", next=i) response = client.post("/login", data=data, follow_redirects=False) else: data = dict(email="matt@lp.com", password="password") response = client.post( f"/login?next={i}", data=data, follow_redirects=False ) if response.status_code in [302, 303]: # this means it passed form validation but should have been quoted assert check_location( app, response.location, oq if oq and (nextloc == "query") else o ) logout(client) def test_get_post_action_redirect(app, client): # test parts of get_post_action_redirect that are hard to get to via the client # e.g. port with app.test_request_context(base_url="https://lp.com:8080/"): r = _get_post_action_redirect( "SECURITY_POST_LOGIN_VIEW", dict(next="https://lp.com:8080/myredirect") ) assert r == "https://lp.com:8080/myredirect" def test_kwargs(): import warnings warnings.simplefilter("error") with pytest.raises(DeprecationWarning): Security(myownkwarg="hi") def test_nodatastore(app): with pytest.raises(ValueError): s = Security(app) s.init_app(app) @pytest.mark.filterwarnings("ignore:.*Replacing login_manager.*:DeprecationWarning") def test_reuse_security_object(sqlalchemy_datastore): # See: https://github.com/pallets-eco/flask-security/issues/518 # Let folks re-use the Security object (mostly for testing). security = Security(datastore=sqlalchemy_datastore) app = Flask(__name__) app.response_class = Response app.debug = True app.config["SECRET_KEY"] = "secret" app.config["TESTING"] = True security.init_app(app) assert hasattr(app, "login_manager") app = Flask(__name__) app.response_class = Response app.debug = True app.config["SECRET_KEY"] = "secret" app.config["TESTING"] = True security.init_app(app) assert hasattr(app, "login_manager") @pytest.mark.settings(static_folder_url="/mystatic/fs") def test_static_url(app, sqlalchemy_datastore): from flask_security import url_for_security from flask import url_for init_app_with_options(app, sqlalchemy_datastore) with app.test_request_context("http://localhost:5001/login"): static_url = url_for_security("static", filename="js/webauthn.js") assert static_url == "/mystatic/fs/js/webauthn.js" static_url = url_for(".static", filename="js/webauthn.js") assert static_url == "/mystatic/fs/js/webauthn.js" def test_multi_app(app, sqlalchemy_datastore): # test that 2 different app with 2 different FS # with USERNAME_ENABLE which dynamically changes the class definition app = Flask(__name__) app.response_class = Response app.debug = True app.config["SECRET_KEY"] = "secret" app.config["TESTING"] = True app.config["SECURITY_USERNAME_ENABLE"] = True security = Security(datastore=sqlalchemy_datastore) security.init_app(app) assert hasattr(security.forms["register_form"].cls, "username") assert "username" in security.user_identity_attributes[1].keys() app = Flask(__name__) app.response_class = Response app.debug = True app.config["SECRET_KEY"] = "secret" app.config["TESTING"] = True app.config["SECURITY_USERNAME_ENABLE"] = True security2 = Security(datastore=sqlalchemy_datastore) security2.init_app(app) assert hasattr(security2.forms["register_form"].cls, "username") assert "username" in security2.user_identity_attributes[1].keys() @pytest.mark.registerable() def test_login_email_whatever(app, client, get_message): # login, by default, shouldn't verify email address is deliverable.. # register etc can/should do that. app.config["SECURITY_EMAIL_VALIDATOR_ARGS"] = {"check_deliverability": True} # register should fail since non-deliverable TLD data = dict( email="dude@me.mytld", password="awesome sunset", ) response = client.post("/register", json=data) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_EMAIL_ADDRESS" ) # login should work since we are just checking for identity response = client.post( "/login", data=dict(email="matt@lp.com", password="password") ) assert response.status_code in [302, 303] assert "/" in response.location @pytest.mark.skip def test_sqlalchemy_session_conn(request, app, tmpdir, realdburl): # test harness for checking connection mgmt by logging all connections from .conftest import sqlalchemy_session_setup ds = sqlalchemy_session_setup( request, app, tmpdir, realdburl, echo_pool="debug", echo="debug" ) init_app_with_options(app, ds) client = app.test_client() client.post("/login", data=dict(email="matt@lp.com", password="password")) client.post("/login", json=dict(noemail="matt@lp.com", password="password")) time.sleep(5) def test_login_required(app, client, get_message): # Test that @login_required calls our default_unauthn_handler from flask_login import login_required @app.route("/loginreq") @login_required def login_req(): pass response = client.get("/loginreq", follow_redirects=False) assert response.location == "/login?next=/loginreq" response = client.get("/loginreq", follow_redirects=True) assert get_message("UNAUTHENTICATED") in response.data def test_simplify_url(): from flask_security.utils import _simplify_url s = _simplify_url("https://localhost/profile", "https://localhost/login") assert s == "/login" s = _simplify_url("https:/myhost/profile", "https://localhost/login") assert s == "https://localhost/login" @pytest.mark.parametrize( "verify_secret_key, verify_fallbacks, should_pass", [ ("new_secret", [], False), # Should fail - only new key ("new_secret", ["old_secret"], True), # Should pass - has fallback ("old_secret", [], True), # Should pass - using original key ("wrong_secret", ["also_wrong"], False), # Should fail - no valid keys ], ids=["new-key-only", "with-fallback", "original-key", "wrong-keys"], ) def test_secret_key_fallbacks(app, verify_secret_key, verify_fallbacks, should_pass): # Create token with original key app.config["SECRET_KEY"] = "old_secret" serializer = _get_serializer(app, "CONFIRM") token = serializer.dumps({"data": "test"}) # Attempt verification with different key configurations app.config["SECRET_KEY"] = verify_secret_key app.config["SECRET_KEY_FALLBACKS"] = verify_fallbacks serializer = _get_serializer(app, "CONFIRM") if should_pass: data = serializer.loads(token) assert data["data"] == "test" else: with pytest.raises(BadTimeSignature): serializer.loads(token) @pytest.mark.settings(username_enable=True) def test_custom_login_form(app, sqlalchemy_datastore, get_message): # Test custom login form that deletes email and uses username only # Also test that if app leave 'email' in as a user identity attribute we # will ignore it class MyLoginForm(LoginForm): email = None app.security = Security( app, datastore=sqlalchemy_datastore, login_form=MyLoginForm, ) populate_data(app) client = app.test_client() response = client.get("/login", follow_redirects=False) assert not get_form_input(response, "email") response = client.post( "/login", json=dict(email="jill@lp.com", password="password") ) assert response.status_code == 400 assert ( get_message("USER_DOES_NOT_EXIST") == response.json["response"]["field_errors"][""][0].encode() ) response = client.post("/login", json=dict(username="jill", password="password")) assert response.status_code == 200 @pytest.mark.settings(password_required=False) def test_password_required_setting(app, sqlalchemy_datastore): with pytest.raises(ValueError) as vex: Security(app=app, datastore=sqlalchemy_datastore) assert "SECURITY_PASSWORD_REQUIRED can only be" in str(vex.value) def test_null_user_id(app, client, get_message): # if DB not configured correctly - make sure we catch null fs_uniquifier json_authenticate(client) assert is_authenticated(client, get_message) with client.session_transaction() as sess: sess["_user_id"] = "" sess["user_id"] = "" assert not is_authenticated(client, get_message) def test_td_format(app, monkeypatch): # Test our internal timedelta formatter - we encourage using humanize but don't # want to require that dependency. import sys monkeypatch.setitem(sys.modules, "humanize", None) assert "4 seconds" == _td_format(timedelta(seconds=4)) assert "1 day" == _td_format(timedelta(days=1)) assert "1 day and 30 minutes" == _td_format(timedelta(days=1, minutes=30)) assert "1 day, 2 hours, 40 seconds" == _td_format(timedelta(hours=26, seconds=40)) def test_td_format_humanize(app): pytest.importorskip("humanize") assert "1 day, 2 hours and 40 seconds" == _td_format( timedelta(hours=26, seconds=40) ) @pytest.mark.parametrize("humanizer", ["fr_FR"], indirect=["humanizer"]) def test_td_format_humanize_fr(app, humanizer): # humanize has built in localization # app responsible for setting it up. assert "1 jour, 2 heures et 40 secondes" == _td_format( timedelta(hours=26, seconds=40) ) @pytest.mark.settings(default_remember_me=True) def test_remember_login_form(app, client): # ensure form has the checkbox set if default set. response = client.get("/login") assert response.status_code == 200 r = get_form_input(response, "remember") assert "checked" in r pallets-eco-flask-security-72bf929/tests/test_oauthglue.py000066400000000000000000000552041525721743500240240ustar00rootroot00000000000000""" test_oauthglue.py ~~~~~~~~~~~~~~~~~ OAuth glue tests - oauthglue is a very thin shim between FS and authlib :copyright: (c) 2022-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import pytest import urllib.parse from urllib.parse import parse_qsl, urlsplit from flask import redirect from flask_wtf import CSRFProtect from flask_security import FsOAuthProvider from tests.test_utils import ( authenticate, capture_flashes, check_location, get_csrf_token, get_form_action, get_form_input_value, get_session, init_app_with_options, is_authenticated, json_authenticate, logout, reset_fresh, setup_tf_sms, ) pytestmark = pytest.mark.oauth() class MockRequestsResponse: # authlib returns a Requests Response def __init__(self, contents): self.contents = contents def json(self): return self.contents class MockProvider: def __init__(self, name): self.name = name self.raise_exception = None self.identity = "matt@lp.com" def set_exception(self, raise_exception): self.raise_exception = raise_exception def set_identity(self, email): self.identity = email def get(self, field, token): resp = MockRequestsResponse({"email": self.identity}) return resp def authorize_access_token(self): if self.raise_exception: raise self.raise_exception return "token" def authorize_redirect(self, uri): redirect_url = f"/whatever?redirect_uri={uri}" return redirect(urllib.parse.quote(redirect_url)) class MockAuthlibFlaskClient: def __init__(self): pass def register(self, name, **kwargs): setattr(self, name, MockProvider(name)) @pytest.mark.settings(oauth_enable=True, post_login_view="/post_login") @pytest.mark.app_settings(wtf_csrf_enabled=True) def test_github(app, sqlalchemy_datastore, get_message): CSRFProtect(app) init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockAuthlibFlaskClient()}}, ) client = app.test_client() response = client.get("/login") github_url = get_form_action(response, 1) csrf_token = get_form_input_value(response, field_id="github_csrf_token") # make sure required CSRF response = client.post(github_url, follow_redirects=False) assert b"The CSRF token is missing" in response.data response = client.post( github_url, data=dict(csrf_token=csrf_token), follow_redirects=False ) assert "/whatever" in response.location response = client.get("/login/oauthresponse/github", follow_redirects=False) assert response.status_code in [302, 303] assert "/post_login" in response.location # verify logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.settings( oauth_enable=True, post_login_view="/post_login", csrf_ignore_unauth_endpoints=True ) @pytest.mark.app_settings(wtf_csrf_enabled=True, wtf_csrf_check_default=False) def test_github_nocsrf(app, sqlalchemy_datastore, get_message): # Test if ignore_unauth_endpoints is true - doesn't require CSRF CSRFProtect(app) init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockAuthlibFlaskClient()}}, ) client = app.test_client() response = client.get("/login") github_url = get_form_action(response, 1) response = client.post(github_url, follow_redirects=False) assert "/whatever" in response.location @pytest.mark.settings(oauth_enable=True, post_login_view="/post_login") def test_outside_register(app, sqlalchemy_datastore, get_message): def myoauth_fetch_identity(oauth, token): resp = oauth.myoauth.get("user", token=token) profile = resp.json() return "email", profile["email"] authlib_oauth = MockAuthlibFlaskClient() authlib_oauth.register("myoauth") init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": authlib_oauth}} ) # Have to register with Oauthglue. app.security.oauthglue.register_provider("myoauth", None, myoauth_fetch_identity) client = app.test_client() response = client.get("/login") myoauth_url = get_form_action(response, 2) response = client.post(myoauth_url, follow_redirects=False) assert "/whatever" in response.location response = client.get("/login/oauthresponse/myoauth", follow_redirects=False) assert response.status_code in [302, 303] assert "/post_login" in response.location # verify logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.settings(oauth_enable=True) def test_bad_api(app, sqlalchemy_datastore, get_message): init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockAuthlibFlaskClient()}}, ) client = app.test_client() response = client.post("/login/oauthstart/foobar") assert response.status_code == 404 response = client.get("/login/oauthresponse/foobar") assert response.status_code == 404 from authlib.integrations.base_client.errors import MismatchingStateError oauth_app = app.security.oauthglue.oauth_app oauth_app.github.set_exception(MismatchingStateError) response = client.get("/login/oauthresponse/github", follow_redirects=True) assert response.status_code == 200 assert ( get_message( "OAUTH_HANDSHAKE_ERROR", exerror="mismatching_state", exdesc="CSRF Warning! State not equal in request and response.", ) in response.data ) @pytest.mark.settings(oauth_enable=True) def test_unknown_user(app, sqlalchemy_datastore, get_message): init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockAuthlibFlaskClient()}}, ) client = app.test_client() oauth_app = app.security.oauthglue.oauth_app oauth_app.github.set_identity("jwag@lp.com") response = client.get("/login/oauthresponse/github", follow_redirects=True) assert get_message("IDENTITY_NOT_REGISTERED", id="jwag@lp.com") in response.data @pytest.mark.settings(oauth_enable=True) def test_disabled_user(app, sqlalchemy_datastore, get_message): init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockAuthlibFlaskClient()}}, ) client = app.test_client() oauth_app = app.security.oauthglue.oauth_app oauth_app.github.set_identity("tiya@lp.com") # disabled account response = client.get("/login/oauthresponse/github", follow_redirects=True) assert check_location(app, response.request.path, "/login") assert get_message("DISABLED_ACCOUNT") in response.data # should be in flash @pytest.mark.settings(oauth_enable=True) def test_unicode_email(app, sqlalchemy_datastore, get_message): # Test that we normalize email response from oauth provider init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockAuthlibFlaskClient()}}, ) client = app.test_client() oauth_app = app.security.oauthglue.oauth_app oauth_app.github.set_identity( "matt@\N{FULLWIDTH LATIN SMALL LETTER L}\N{FULLWIDTH LATIN SMALL LETTER P}.com" ) client.get("/login/oauthresponse/github", follow_redirects=True) assert is_authenticated(client, get_message) @pytest.mark.settings( oauth_enable=True, USER_IDENTITY_ATTRIBUTES=[ {"username": {"mapper": lambda x: x}}, ], ) def test_email_not_identity(app, sqlalchemy_datastore, get_message): # Test that if email isn't an identity attribute - can't use oauth to log in init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockAuthlibFlaskClient()}}, ) client = app.test_client() response = client.get("/login/oauthresponse/github", follow_redirects=True) assert ( b": (Config error - Invalid identity attribute: email). Please try again." in response.data ) def _locked(self, form_error): if self.email == "gal@lp.com": form_error.append("You are not allowed to do that") return True return False @pytest.mark.app_settings(TESTING_USER_INJECT=dict(is_locked=_locked)) @pytest.mark.settings(oauth_enable=True) def test_override_user_locked(app, sqlalchemy_datastore, get_message): init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockAuthlibFlaskClient()}}, ) client = app.test_client() oauth_app = app.security.oauthglue.oauth_app oauth_app.github.set_identity("gal@lp.com") # disabled account response = client.get("/login/oauthresponse/github", follow_redirects=True) assert check_location(app, response.request.path, "/login") assert b"You are not allowed to do that" in response.data # should be in flash @pytest.mark.two_factor() @pytest.mark.settings(oauth_enable=True) def test_tf(app, sqlalchemy_datastore, get_message): init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockAuthlibFlaskClient()}}, ) client = app.test_client() authenticate(client) sms_sender = setup_tf_sms(client) logout(client) response = client.get("/login?next=/profile") github_url = get_form_action(response, 1) response = client.post(github_url, follow_redirects=False) assert "/whatever" in response.location redirect_url = urllib.parse.urlsplit(urllib.parse.unquote(response.location)) local_redirect = urllib.parse.parse_qs(redirect_url.query)["redirect_uri"][0] response = client.get(local_redirect, follow_redirects=True) sendcode_url = get_form_action(response, 0) response = client.post( sendcode_url, data=dict(code=sms_sender.messages[0].split()[-1]), follow_redirects=True, ) assert b"Profile Page" in response.data @pytest.mark.settings( oauth_enable=True, redirect_host="myui.com:8090", redirect_behavior="spa", login_error_view="/login-error", post_oauth_login_view="/post-login", csrf_ignore_unauth_endpoints=False, ) @pytest.mark.app_settings(wtf_csrf_enabled=True) def test_spa(app, sqlalchemy_datastore, get_message): CSRFProtect(app) headers = {"Accept": "application/json", "Content-Type": "application/json"} init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockAuthlibFlaskClient()}}, ) client = app.test_client() csrf_token = get_csrf_token(client) headers["X-CSRF-Token"] = csrf_token response = client.post("/login/oauthstart/github", headers=headers) assert "/whatever" in response.location redirect_url = urllib.parse.urlsplit(urllib.parse.unquote(response.location)) local_redirect = urllib.parse.parse_qs(redirect_url.query)["redirect_uri"][0] response = client.get(local_redirect, headers=headers) assert response.status_code in [302, 303] split = urlsplit(response.location) assert "myui.com:8090" == split.netloc assert "/post-login" == split.path qparams = dict(parse_qsl(split.query)) assert qparams["email"] == "matt@lp.com" # try unknown user - should redirect to login_error_view oauth_app = app.security.oauthglue.oauth_app oauth_app.github.set_identity("jwag@lp.com") response = client.get("/login/oauthresponse/github", follow_redirects=False) split = urlsplit(response.location) assert "/login-error" == split.path qparams = dict(parse_qsl(split.query)) assert ( qparams["error"] == get_message("IDENTITY_NOT_REGISTERED", id="jwag@lp.com").decode() ) # try fake oauth exception from authlib.integrations.base_client.errors import MismatchingStateError oauth_app.github.set_exception(MismatchingStateError) response = client.get("/login/oauthresponse/github", follow_redirects=False) split = urlsplit(response.location) assert "/login-error" == split.path qparams = dict(parse_qsl(split.query)) msg = get_message( "OAUTH_HANDSHAKE_ERROR", exerror="mismatching_state", exdesc="CSRF Warning! State not equal in request and response.", ) assert qparams["error"] == msg.decode() @pytest.mark.settings(oauth_enable=True, post_login_view="/post-login") def test_already_auth(app, sqlalchemy_datastore, get_message): headers = {"Accept": "application/json", "Content-Type": "application/json"} init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockAuthlibFlaskClient()}}, ) client = app.test_client() authenticate(client) assert is_authenticated(client, get_message) # json response = client.post("/login/oauthstart/github", headers=headers) assert response.status_code == 400 # forms response = client.post("/login/oauthstart/github", follow_redirects=False) assert response.status_code in [302, 303] check_location(app, response.location, "/post-login") @pytest.mark.settings(oauth_enable=True, post_login_view="/post-login") def test_simple_next(app, sqlalchemy_datastore, get_message): # For oauth we stash 'next' in the session since we can't really # send it all around the oauth providers. init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockAuthlibFlaskClient()}}, ) client = app.test_client() response = client.get("/profile", follow_redirects=True) github_url = get_form_action(response, 1) response = client.post(github_url, follow_redirects=False) assert "/whatever" in response.location session = get_session(response) assert "fs_oauth_next" in session response = client.get("/login/oauthresponse/github", follow_redirects=False) assert response.status_code in [302, 303] assert check_location(app, response.location, "/profile") session = get_session(response) assert "fs_oauth_next" not in session @pytest.mark.settings(oauth_enable=True, post_login_view="/post_login") def test_provider_class(app, sqlalchemy_datastore, get_message): from authlib.integrations.base_client.errors import MismatchingStateError class MyOauthProvider(FsOAuthProvider): def fetch_identity_cb(self, oauth, token): resp = oauth.myoauth.get("user", token=token) profile = resp.json() return "email", profile["email"] def oauth_response_failure(self, error_view, e): assert isinstance(e, MismatchingStateError) return redirect("/uh-oh") init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockAuthlibFlaskClient()}}, ) # Have to register with Oauthglue. app.security.oauthglue.register_provider_ext(MyOauthProvider("myoauth")) client = app.test_client() response = client.get("/login") myoauth_url = get_form_action(response, 2) response = client.post(myoauth_url, follow_redirects=False) assert "/whatever" in response.location # test error - and that our handler is called oauth_app = app.security.oauthglue.oauth_app oauth_app.myoauth.set_exception(MismatchingStateError) response = client.get("/login/oauthresponse/myoauth", follow_redirects=False) assert response.status_code in [302, 303] assert check_location(app, response.location, "/uh-oh") # now log in successfully oauth_app.myoauth.set_exception(None) response = client.get("/login/oauthresponse/myoauth", follow_redirects=False) assert response.status_code in [302, 303] assert check_location(app, response.location, "/post_login") assert is_authenticated(client, get_message) @pytest.mark.settings(oauth_enable=True, post_verify_view="/post_verify") def test_verify(app, sqlalchemy_datastore, get_message): init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockAuthlibFlaskClient()}}, ) client = app.test_client() # just authenticate using github client.get("/login/oauthresponse/github", follow_redirects=False) assert is_authenticated(client, get_message) reset_fresh(client, app.config["SECURITY_FRESHNESS"]) # This will return the /verify form response = client.get("/fresh", follow_redirects=True) github_url = get_form_action(response, 1) assert "oauth-verify-start" in github_url response = client.post(github_url, follow_redirects=False) assert "/whatever" in response.location session = get_session(response) assert "fs_oauth_next" in session with capture_flashes() as flashes: response = client.get( "/login/oauth-verify-response/github", follow_redirects=True ) assert flashes[0]["category"] == "info" assert flashes[0]["message"].encode("utf-8") == get_message( "REAUTHENTICATION_SUCCESSFUL" ) assert b"Fresh Only" in response.data session = get_session(response) assert "fs_oauth_next" not in session @pytest.mark.settings(oauth_enable=True) def test_verify_unknown_user(app, sqlalchemy_datastore, get_message): init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockAuthlibFlaskClient()}}, ) client = app.test_client() authenticate(client) oauth_app = app.security.oauthglue.oauth_app oauth_app.github.set_identity("jwag@lp.com") # this will cause response to fail reset_fresh(client, app.config["SECURITY_FRESHNESS"]) response = client.get("/fresh", follow_redirects=True) github_url = get_form_action(response, 1) response = client.post(github_url, follow_redirects=False) with capture_flashes() as flashes: redirect_url = urllib.parse.urlsplit(urllib.parse.unquote(response.location)) local_redirect = urllib.parse.parse_qs(redirect_url.query)["redirect_uri"][0] response = client.get(local_redirect, follow_redirects=True) assert flashes[0]["category"] == "error" assert flashes[0]["message"].encode("utf-8") == get_message( "IDENTITY_NOT_REGISTERED", id="jwag@lp.com" ) assert b"Reauthenticate" in response.data session = get_session(response) assert "fs_oauth_next" not in session @pytest.mark.settings(oauth_enable=True) def test_verify_evil_user(app, sqlalchemy_datastore, get_message): # Ensure a valid (evil) user can't use their oauth credentials to 'verify' # another user. init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockAuthlibFlaskClient()}}, ) client = app.test_client() authenticate(client) # authenticate as matt@lp.com oauth_app = app.security.oauthglue.oauth_app oauth_app.github.set_identity("joe@lp.com") # a valid user reset_fresh(client, app.config["SECURITY_FRESHNESS"]) response = client.get("/fresh", follow_redirects=True) github_url = get_form_action(response, 1) response = client.post(github_url, follow_redirects=False) with capture_flashes() as flashes: redirect_url = urllib.parse.urlsplit(urllib.parse.unquote(response.location)) local_redirect = urllib.parse.parse_qs(redirect_url.query)["redirect_uri"][0] response = client.get(local_redirect, follow_redirects=True) assert flashes[0]["category"] == "error" assert flashes[0]["message"].encode("utf-8") == get_message( "IDENTITY_NOT_REGISTERED", id="joe@lp.com" ) assert b"Reauthenticate" in response.data session = get_session(response) assert "fs_oauth_next" not in session @pytest.mark.settings( oauth_enable=True, oauth_builtin_providers=["github"], post_oauth_verify_view="/post_oauth_verify", redirect_host="myui.com:8090", redirect_behavior="spa", ) def test_verify_spa(app, sqlalchemy_datastore, get_message): headers = {"Accept": "application/json", "Content-Type": "application/json"} init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockAuthlibFlaskClient()}}, ) client = app.test_client() json_authenticate(client) reset_fresh(client, app.config["SECURITY_FRESHNESS"]) response = client.get("/fresh", headers=headers) assert response.json["response"]["reauth_required"] assert response.json["response"]["oauth_enabled"] assert response.json["response"]["oauth_providers"] == ["github"] response = client.post("/login/oauth-verify-start/github", follow_redirects=False) assert "/whatever" in response.location # this is what github would call response = client.get("/login/oauth-verify-response/github", follow_redirects=False) assert ( response.location == "//myui.com:8090/post_oauth_verify?" "email=matt%40lp.com&identity=matt%40lp.com" ) response = client.get("/verify", headers=headers) assert response.json["response"]["oauth_enabled"] assert response.json["response"]["oauth_providers"] == ["github"] @pytest.mark.settings( oauth_enable=True, redirect_behavior="spa", verify_error_view="/verify-error" ) def test_verify_unknown_user_spa(app, sqlalchemy_datastore, get_message): init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockAuthlibFlaskClient()}}, ) client = app.test_client() json_authenticate(client) reset_fresh(client, app.config["SECURITY_FRESHNESS"]) oauth_app = app.security.oauthglue.oauth_app oauth_app.github.set_identity("jwag@lp.com") # what github would have called response = client.get("/login/oauth-verify-response/github") assert "/verify-error" in response.location assert "not+registered" in response.location @pytest.mark.settings( oauth_enable=True, redirect_behavior="spa", verify_error_view="/verify-error" ) def test_verify_spa_exc(app, sqlalchemy_datastore, get_message): init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"oauth": MockAuthlibFlaskClient()}}, ) client = app.test_client() json_authenticate(client) # try fake oauth exception from authlib.integrations.base_client.errors import MismatchingStateError oauth_app = app.security.oauthglue.oauth_app oauth_app.github.set_exception(MismatchingStateError) response = client.get("/login/oauth-verify-response/github", follow_redirects=False) split = urlsplit(response.location) assert "/verify-error" == split.path qparams = dict(parse_qsl(split.query)) msg = get_message( "OAUTH_HANDSHAKE_ERROR", exerror="mismatching_state", exdesc="CSRF Warning! State not equal in request and response.", ) assert qparams["error"] == msg.decode() pallets-eco-flask-security-72bf929/tests/test_passwordless.py000066400000000000000000000203431525721743500245540ustar00rootroot00000000000000""" test_passwordless ~~~~~~~~~~~~~~~~~ Passwordless tests """ from datetime import date, timedelta import re from urllib.parse import parse_qsl, urlsplit import warnings import pytest from flask import Flask from freezegun import freeze_time from tests.test_utils import ( capture_flashes, capture_passwordless_login_requests, logout, ) from flask_security import Security, UserMixin, login_instructions_sent pytestmark = pytest.mark.passwordless() def test_passwordless_flag(app, client, get_message, outbox): recorded = [] @login_instructions_sent.connect_via(app) def on_instructions_sent(app, user, login_token): assert isinstance(app, Flask) assert isinstance(user, UserMixin) assert isinstance(login_token, str) recorded.append(user) # Test disabled account response = client.post( "/login", data=dict(email="tiya@lp.com"), follow_redirects=True ) assert get_message("DISABLED_ACCOUNT") in response.data # Test login with json and valid email data = dict(email="matt@lp.com") response = client.post( "/login", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 200 assert len(recorded) == 1 assert len(outbox) == 1 # Test login with json and invalid email data = dict(email="nobody@lp.com") response = client.post( "/login", json=data, headers={"Content-Type": "application/json"} ) assert b"errors" in response.data # Test sends email and shows appropriate response with capture_passwordless_login_requests() as requests: response = client.post( "/login", data=dict(email="matt@lp.com"), follow_redirects=True ) assert len(recorded) == 2 assert len(requests) == 1 assert len(outbox) == 2 assert "user" in requests[0] assert "login_token" in requests[0] user = requests[0]["user"] assert get_message("LOGIN_EMAIL_SENT", email=user.email) in response.data token = requests[0]["login_token"] response = client.get("/login/" + token, follow_redirects=True) assert get_message("PASSWORDLESS_LOGIN_SUCCESSFUL") in response.data # Test already authenticated response = client.get("/login/" + token, follow_redirects=True) assert get_message("PASSWORDLESS_LOGIN_SUCCESSFUL") not in response.data logout(client) # Test invalid token response = client.get("/login/bogus", follow_redirects=True) assert get_message("INVALID_LOGIN_TOKEN") in response.data # Test login request with invalid email response = client.post("/login", data=dict(email="bogus@bogus.com")) assert get_message("USER_DOES_NOT_EXIST") in response.data def test_passwordless_template(app, client, get_message, outbox): # Check contents of email template - this uses a test template # in order to check all context vars since the default template # doesn't have all of them. with capture_passwordless_login_requests() as requests: client.post("/login", data=dict(email="joe@lp.com"), follow_redirects=True) assert len(outbox) == 1 matcher = re.findall(r"\w+:.*", outbox[0].body, re.IGNORECASE) # should be 4 - link, email, token, config item assert matcher[1].split(":")[1] == "joe@lp.com" assert matcher[2].split(":")[1] == requests[0]["login_token"] assert matcher[3].split(":")[1] == "True" # register_blueprint # check link link = matcher[0].split(":", 1)[1] response = client.get(link, follow_redirects=True) assert get_message("PASSWORDLESS_LOGIN_SUCCESSFUL") in response.data @pytest.mark.settings(login_within=timedelta(days=1)) def test_expired_login_token(client, app, get_message): e = "matt@lp.com" with capture_passwordless_login_requests() as requests: # Note that we need relatively new-ish date since session cookies also expire. with freeze_time(date.today() + timedelta(days=-1)): client.post("/login", data=dict(email=e), follow_redirects=True) token = requests[0]["login_token"] user = requests[0]["user"] response = client.get("/login/" + token, follow_redirects=True) assert ( get_message("LOGIN_EXPIRED", within="1 day", email=user.email) in response.data ) @pytest.mark.settings( redirect_host="localhost:8081", redirect_behavior="spa", post_login_view="/login-redirect", ) def test_spa_get(app, client): """ Test 'single-page-application' style redirects This uses json only. """ with capture_flashes() as flashes: with capture_passwordless_login_requests() as requests: response = client.post( "/login", json=dict(email="matt@lp.com"), headers={"Content-Type": "application/json"}, ) assert response.headers["Content-Type"] == "application/json" token = requests[0]["login_token"] response = client.get("/login/" + token) assert response.status_code in [302, 303] split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/login-redirect" == split.path qparams = dict(parse_qsl(split.query)) assert qparams["email"] == "matt@lp.com" assert len(flashes) == 0 @pytest.mark.settings( login_within=timedelta(seconds=73), redirect_host="localhost:8081", redirect_behavior="spa", login_error_view="/login-error", ) def test_spa_get_bad_token(app, client, get_message): """Test expired and invalid token""" with capture_flashes() as flashes: with capture_passwordless_login_requests() as requests: # Note that we need relatively new-ish date since session cookies expire. with freeze_time(date.today() + timedelta(days=-1)): response = client.post( "/login", json=dict(email="matt@lp.com"), headers={"Content-Type": "application/json"}, ) assert response.headers["Content-Type"] == "application/json" token = requests[0]["login_token"] response = client.get("/login/" + token) assert response.status_code in [302, 303] split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/login-error" == split.path qparams = dict(parse_qsl(split.query)) assert all(k in qparams for k in ["email", "error", "identity"]) msg = get_message( "LOGIN_EXPIRED", within="1 minute and 13 seconds", email="matt@lp.com" ) assert msg == qparams["error"].encode("utf-8") # Test mangled token token = ( "WyIxNjQ2MzYiLCIxMzQ1YzBlZmVhM2VhZjYwODgwMDhhZGU2YzU0MzZjMiJd." "BZEw_Q.lQyo3npdPZtcJ_sNHVHP103syjM" "&url_id=fbb89a8328e58c181ea7d064c2987874bc54a23d" ) response = client.get("/login/" + token) assert response.status_code in [302, 303] split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/login-error" == split.path qparams = dict(parse_qsl(split.query)) assert len(qparams) == 1 assert all(k in qparams for k in ["error"]) msg = get_message("INVALID_LOGIN_TOKEN") assert msg == qparams["error"].encode("utf-8") assert len(flashes) == 0 def test_deprecated(app, sqlalchemy_datastore): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") security = Security() security.init_app(app, sqlalchemy_datastore) assert any("passwordless feature" in str(m.message) for m in w) def _locked(self, form_error): if self.email == "gal@lp.com": form_error.append("You are not allowed to do that") return True return False @pytest.mark.app_settings(TESTING_USER_INJECT=dict(is_locked=_locked)) def test_override_user_locked(app, client, get_message): response = client.post("/login", json=dict(email="gal@lp.com")) assert response.status_code == 400 assert response.json["response"]["errors"] == ["You are not allowed to do that"] assert response.json["response"]["field_errors"]["email"] == [ "You are not allowed to do that" ] pallets-eco-flask-security-72bf929/tests/test_phone_util.py000066400000000000000000000012241525721743500241660ustar00rootroot00000000000000""" test_phone_util ~~~~~~~~~~~~~~~ Tests for PhoneUtil class in flask_security.phone_util. Covers phone number validation and canonicalization logic. """ import pytest from flask_security.phone_util import PhoneUtil # Use default app fixture from conftest.py # Override config using the recommended settings marker @pytest.mark.settings(phone_region_default="US") @pytest.mark.parametrize( "input_number", ["123456", "bad-number-%%%", "+999", "abcdefgh"] ) def test_invalid_phone_numbers_return_none(app, input_number): with app.app_context(): phone_util = PhoneUtil(app) assert phone_util.get_canonical_form(input_number) is None pallets-eco-flask-security-72bf929/tests/test_recoverable.py000066400000000000000000001117311525721743500243160ustar00rootroot00000000000000""" test_recoverable ~~~~~~~~~~~~~~~~ Recoverable (password, usernanme) functionality tests :copyright: (c) 2019-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from datetime import date, timedelta import re from urllib.parse import parse_qsl, urlsplit import pytest from flask import Flask from freezegun import freeze_time from wtforms.fields import StringField from wtforms.validators import Length from tests.test_utils import ( authenticate, capture_flashes, capture_reset_password_requests, check_location, check_signals, get_form_input_value, is_authenticated, logout, populate_data, ) from flask_security.core import Security, UserMixin from flask_security.forms import ForgotPasswordForm, LoginForm from flask_security.signals import ( password_reset, reset_password_instructions_sent, ) pytestmark = pytest.mark.recoverable() def test_recoverable_flag(app, clients, get_message, outbox): recorded_resets = [] recorded_instructions_sent = [] @password_reset.connect_via(app) def on_password_reset(app, user): recorded_resets.append(user) @reset_password_instructions_sent.connect_via(app) def on_instructions_sent(app, **kwargs): assert isinstance(app, Flask) assert isinstance(kwargs["user"], UserMixin) assert isinstance(kwargs["token"], str) recorded_instructions_sent.append(kwargs["user"]) # Test the reset view response = clients.get("/reset") assert b"

    Send password reset instructions

    " in response.data assert re.search(b']*type="email"[^>]*>', response.data) # Test submitting email to reset password creates a token and sends email with capture_reset_password_requests() as requests: response = clients.post( "/reset", data=dict(email="joe@lp.com"), follow_redirects=True ) assert len(recorded_instructions_sent) == 1 assert len(outbox) == 1 assert response.status_code == 200 assert get_message("PASSWORD_RESET_REQUEST", email="joe@lp.com") in response.data token = requests[0]["token"] # Test view for reset token response = clients.get("/reset/" + token) assert b"

    Reset password

    " in response.data # Test submitting a new password but leave out confirm response = clients.post( "/reset/" + token, data={"password": "newpassword"}, follow_redirects=True ) assert get_message("PASSWORD_NOT_PROVIDED") in response.data assert len(recorded_resets) == 0 # Test submitting a new password response = clients.post( "/reset/" + token, data={"password": "awesome sunset", "password_confirm": "awesome sunset"}, follow_redirects=True, ) assert get_message("PASSWORD_RESET_NO_LOGIN") in response.data assert len(recorded_resets) == 1 logout(clients) # Test logging in with the new password response = authenticate( clients, "joe@lp.com", "awesome sunset", follow_redirects=True ) assert b"Welcome joe@lp.com" in response.data logout(clients) # Test invalid email response = clients.post( "/reset", data=dict(email="bogus@lp.com"), follow_redirects=True ) assert get_message("USER_DOES_NOT_EXIST") in response.data logout(clients) # Test invalid token response = clients.post( "/reset/bogus", data={"password": "awesome sunset", "password_confirm": "awesome sunset"}, follow_redirects=True, ) assert get_message("INVALID_RESET_PASSWORD_TOKEN") in response.data # Test mangled token token = ( "WyIxNjQ2MzYiLCIxMzQ1YzBlZmVhM2VhZjYwODgwMDhhZGU2YzU0MzZjMiJd." "BZEw_Q.lQyo3npdPZtcJ_sNHVHP103syjM" "&url_id=fbb89a8328e58c181ea7d064c2987874bc54a23d" ) response = clients.post( "/reset/" + token, data={"password": "newpassword", "password_confirm": "newpassword"}, follow_redirects=True, ) assert get_message("INVALID_RESET_PASSWORD_TOKEN") in response.data @pytest.mark.confirmable() @pytest.mark.settings(requires_confirmation_error_view="/confirm") def test_requires_confirmation_error_redirect(app, clients): response = clients.post( "/reset", data=dict(email="joe@lp.com"), follow_redirects=True ) assert b"send_confirmation_form" in response.data assert b"joe@lp.com" in response.data @pytest.mark.settings() def test_recoverable_json(app, client, get_message, outbox): recorded_resets = [] recorded_instructions_sent = [] @password_reset.connect_via(app) def on_password_reset(app, user): recorded_resets.append(user) @reset_password_instructions_sent.connect_via(app) def on_instructions_sent(app, **kwargs): recorded_instructions_sent.append(kwargs["user"]) with capture_flashes() as flashes: # Test reset password creates a token and sends email with capture_reset_password_requests() as requests: response = client.post( "/reset", json=dict(email="joe@lp.com"), headers={"Content-Type": "application/json"}, ) assert response.headers["Content-Type"] == "application/json" assert len(recorded_instructions_sent) == 1 assert len(outbox) == 1 assert response.status_code == 200 token = requests[0]["token"] # Test invalid email response = client.post( "/reset", json=dict(email="whoknows@lp.com"), ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "USER_DOES_NOT_EXIST" ) # Test submitting a new password but leave out 'confirm' response = client.post( "/reset/" + token, json=dict(password="newpassword"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "PASSWORD_NOT_PROVIDED" ) # Test submitting a new password response = client.post( "/reset/" + token + "?include_auth_token", json=dict(password="awesome sunset", password_confirm="awesome sunset"), ) assert not response.json["response"] assert len(recorded_resets) == 1 # reset automatically logs user in logout(client) # Test logging in with the new password response = client.post( "/login?include_auth_token", json=dict(email="joe@lp.com", password="awesome sunset"), headers={"Content-Type": "application/json"}, ) assert all( k in response.json["response"]["user"] for k in ["email", "authentication_token"] ) logout(client) # Use token again - should fail since already have set new password. response = client.post( "/reset/" + token, json=dict(password="newpassword", password_confirm="newpassword"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 400 assert len(recorded_resets) == 1 # Test invalid token response = client.post( "/reset/bogus", json=dict(password="newpassword", password_confirm="newpassword"), headers={"Content-Type": "application/json"}, ) assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_RESET_PASSWORD_TOKEN" ) assert len(flashes) == 0 @pytest.mark.registerable() @pytest.mark.settings( email_template_password_reset="security/email/reset_instructions_test", email_html=False, reset_password_within=timedelta(minutes=45), ) def test_recoverable_template(app, client, get_message, outbox): # Check contents of email template - this uses a test template # in order to check all context vars since the default template # doesn't have all of them. with capture_reset_password_requests() as resets: response = client.post( "/reset", data=dict(email="joe@lp.com"), follow_redirects=True ) assert len(outbox) == 1 matcher = re.findall(r"\w+:.*", outbox[0].body, re.IGNORECASE) # should be 5 - link, email, token, config item, within assert matcher[1].split(":")[1] == "joe@lp.com" assert matcher[2].split(":")[1] == resets[0]["reset_token"] assert matcher[3].split(":")[1] == "True" # register_blueprint assert matcher[4].split(":")[1] == "/reset" # SECURITY_RESET_URL assert matcher[5].split(":")[1] == "45 minutes" # within # check link link = matcher[0].split(":", 1)[1] response = client.get(link, follow_redirects=True) assert b"Reset Password" in response.data # regular text template is included in the test template assert "This link will expire in 45 minutes." in outbox[0].body def test_recover_invalidates_session(app, client): # Make sure that if we reset our password - prior sessions are invalidated. other_client = app.test_client() authenticate(other_client) response = other_client.get("/profile", follow_redirects=True) assert b"Profile Page" in response.data # use normal client to reset password with capture_reset_password_requests() as requests: response = client.post( "/reset", json=dict(email="matt@lp.com"), ) assert response.headers["Content-Type"] == "application/json" assert response.status_code == 200 token = requests[0]["token"] # Test submitting a new password response = client.post( "/reset/" + token + "?include_auth_token", json=dict(password="awesome sunset", password_confirm="awesome sunset"), ) assert response.status_code == 200 # try to access protected endpoint with old session - shouldn't work response = other_client.get("/profile") assert response.status_code in [302, 303] assert response.location == "/login?next=/profile" def test_login_form_description(app, sqlalchemy_datastore): app.security = Security(app, datastore=sqlalchemy_datastore) with app.test_request_context("/login"): login_form = LoginForm() expected = 'Forgot password?' assert login_form.password.description == expected @pytest.mark.settings(reset_password_within=timedelta(hours=4)) def test_expired_reset_token(client, get_message): # Note that we need relatively new-ish date since session cookies also expire. with freeze_time(date.today() + timedelta(days=-1)): with capture_reset_password_requests() as requests: client.post("/reset", data=dict(email="joe@lp.com"), follow_redirects=True) user = requests[0]["user"] token = requests[0]["token"] with capture_flashes() as flashes: msg = get_message("PASSWORD_RESET_EXPIRED", within="4 hours", email=user.email) # Test getting reset form with expired token response = client.get("/reset/" + token, follow_redirects=True) assert msg in response.data # Test trying to reset password with expired token response = client.post( "/reset/" + token, data={"password": "newpassword", "password_confirm": "newpassword"}, follow_redirects=True, ) assert msg in response.data assert len(flashes) == 2 def test_bad_reset_token(client, get_message): # Test invalid token - get form response = client.get("/reset/bogus", follow_redirects=True) assert get_message("INVALID_RESET_PASSWORD_TOKEN") in response.data # Test invalid token - reset password response = client.post( "/reset/bogus", data={"password": "newpassword", "password_confirm": "newpassword"}, follow_redirects=True, ) assert get_message("INVALID_RESET_PASSWORD_TOKEN") in response.data # Test mangled token token = ( "WyIxNjQ2MzYiLCIxMzQ1YzBlZmVhM2VhZjYwODgwMDhhZGU2YzU0MzZjMiJd." "BZEw_Q.lQyo3npdPZtcJ_sNHVHP103syjM" "&url_id=fbb89a8328e58c181ea7d064c2987874bc54a23d" ) response = client.post( "/reset/" + token, data={"password": "newpassword", "password_confirm": "newpassword"}, follow_redirects=True, ) assert get_message("INVALID_RESET_PASSWORD_TOKEN") in response.data def test_reset_token_deleted_user(app, client, get_message): with capture_reset_password_requests() as requests: client.post("/reset", data=dict(email="gene@lp.com"), follow_redirects=True) token = requests[0]["token"] # Delete user with app.app_context(): # load user (and role) to get into session so cascade delete works. user = app.security.datastore.find_user(email="gene@lp.com") app.security.datastore.delete(user) app.security.datastore.commit() response = client.post( "/reset/" + token, data={"password": "newpassword", "password_confirm": "newpassword"}, follow_redirects=True, ) msg = get_message("INVALID_RESET_PASSWORD_TOKEN") assert msg in response.data def test_used_reset_token(client, get_message): with capture_reset_password_requests() as requests: client.post("/reset", data=dict(email="joe@lp.com"), follow_redirects=True) token = requests[0]["token"] # use the token response = client.post( "/reset/" + token, data={"password": "awesome sunset", "password_confirm": "awesome sunset"}, follow_redirects=True, ) assert get_message("PASSWORD_RESET_NO_LOGIN") in response.data logout(client) # attempt to use it a second time response2 = client.post( "/reset/" + token, data={"password": "otherpassword", "password_confirm": "otherpassword"}, follow_redirects=True, ) msg = get_message("INVALID_RESET_PASSWORD_TOKEN") assert msg in response2.data def test_reset_passwordless_user(client, get_message): with capture_reset_password_requests() as requests: client.post("/reset", data=dict(email="jess@lp.com"), follow_redirects=True) token = requests[0]["token"] # use the token response = client.post( "/reset/" + token, data={"password": "awesome sunset", "password_confirm": "awesome sunset"}, follow_redirects=True, ) assert get_message("PASSWORD_RESET_NO_LOGIN") in response.data @pytest.mark.settings(reset_url="/custom_reset") def test_custom_reset_url(client): response = client.get("/custom_reset") assert response.status_code == 200 @pytest.mark.settings( reset_password_template="custom_security/reset_password.html", forgot_password_template="custom_security/forgot_password.html", ) def test_custom_reset_templates(client): response = client.get("/reset") assert b"CUSTOM FORGOT PASSWORD" in response.data with capture_reset_password_requests() as requests: client.post("/reset", data=dict(email="joe@lp.com"), follow_redirects=True) token = requests[0]["token"] response = client.get("/reset/" + token) assert b"CUSTOM RESET PASSWORD" in response.data @pytest.mark.settings( redirect_host="myui.com:8090", redirect_behavior="spa", reset_view="/reset-redirect", ) def test_spa_get(app, client): """ Test 'single-page-application' style redirects This uses json only. """ with capture_reset_password_requests() as requests: response = client.post( "/reset", json=dict(email="joe@lp.com"), ) assert response.headers["Content-Type"] == "application/json" assert "user" not in response.json["response"] token = requests[0]["token"] response = client.get("/reset/" + token) assert response.status_code in [302, 303] split = urlsplit(response.headers["Location"]) assert "myui.com:8090" == split.netloc assert "/reset-redirect" == split.path qparams = dict(parse_qsl(split.query)) # we shouldn't be showing PII assert "email" not in qparams assert qparams["token"] == token @pytest.mark.settings( reset_password_within=timedelta(seconds=39), redirect_host="localhost:8081", redirect_behavior="spa", reset_error_view="/reset-error", ) def test_spa_get_bad_token(app, client, get_message): """Test expired and invalid token""" with capture_flashes() as flashes: # Note that we need relatively new-ish date since session cookies also expire. with freeze_time(date.today() + timedelta(days=-1)): with capture_reset_password_requests() as requests: response = client.post( "/reset", json=dict(email="joe@lp.com"), headers={"Content-Type": "application/json"}, ) assert response.headers["Content-Type"] == "application/json" assert "user" not in response.json["response"] token = requests[0]["token"] response = client.get("/reset/" + token) assert response.status_code in [302, 303] split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/reset-error" == split.path qparams = dict(parse_qsl(split.query)) # on error - no PII should be returned. assert "error" in qparams assert "identity" not in qparams assert "email" not in qparams msg = get_message( "PASSWORD_RESET_EXPIRED", within="39 seconds", email="joe@lp.com" ) assert msg == qparams["error"].encode("utf-8") # Test mangled token token = ( "WyIxNjQ2MzYiLCIxMzQ1YzBlZmVhM2VhZjYwODgwMDhhZGU2YzU0MzZjMiJd." "BZEw_Q.lQyo3npdPZtcJ_sNHVHP103syjM" "&url_id=fbb89a8328e58c181ea7d064c2987874bc54a23d" ) response = client.get("/reset/" + token) assert response.status_code in [302, 303] split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/reset-error" == split.path qparams = dict(parse_qsl(split.query)) assert len(qparams) == 1 assert all(k in qparams for k in ["error"]) msg = get_message("INVALID_RESET_PASSWORD_TOKEN") assert msg == qparams["error"].encode("utf-8") assert len(flashes) == 0 @pytest.mark.settings(password_complexity_checker="zxcvbn") def test_easy_password(client, get_message): with capture_reset_password_requests() as requests: client.post("/reset", data=dict(email="joe@lp.com"), follow_redirects=True) token = requests[0]["token"] # use the token response = client.post( "/reset/" + token, data={"password": "mypassword", "password_confirm": "mypassword"}, follow_redirects=True, ) assert b"This is a very common password" in response.data def test_reset_inactive(client, get_message): response = client.post( "/reset", data=dict(email="tiya@lp.com"), follow_redirects=True ) assert get_message("DISABLED_ACCOUNT") in response.data response = client.post( "/reset", json=dict(email="tiya@lp.com"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 400 def test_email_normalization(client, get_message): response = client.post( "/reset", data=dict(email="joe@LP.COM"), follow_redirects=True ) assert response.status_code == 200 assert get_message("PASSWORD_RESET_REQUEST", email="joe@lp.com") in response.data def test_password_normalization(app, client, get_message): with capture_reset_password_requests() as requests: response = client.post( "/reset", json=dict(email="matt@lp.com"), ) assert response.status_code == 200 token = requests[0]["token"] response = client.post( "/reset/" + token, json=dict(password="HöheHöhe", password_confirm="HöheHöhe"), ) assert response.status_code == 200 logout(client) # make sure can log in with new password both normalized or not response = client.post( "/login", json=dict(email="matt@lp.com", password="HöheHöhe"), ) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 logout(client) response = client.post( "/login", json=dict(email="matt@lp.com", password="Ho\u0308heHo\u0308he"), ) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.settings(return_generic_responses=True) def test_generic_response(app, client, get_message): # try unknown user response = client.post("/reset", data=dict(email="whoami@test.com")) assert ( get_message("PASSWORD_RESET_REQUEST", email="whoami@test.com") in response.data ) response = client.post("/reset", json=dict(email="whoami@test.com")) assert response.status_code == 200 assert not any(e in response.json["response"].keys() for e in ["error", "errors"]) def test_generic_with_extra(app, sqlalchemy_datastore): # If application adds a field, make sure we properly return errors # even if 'RETURN_GENERIC_RESPONSES' is set. class MyForgotPasswordForm(ForgotPasswordForm): recaptcha = StringField("Recaptcha", validators=[Length(min=5)]) app.config["SECURITY_RETURN_GENERIC_RESPONSES"] = True app.config["SECURITY_FORGOT_PASSWORD_TEMPLATE"] = "generic_reset.html" app.security = Security( app, datastore=sqlalchemy_datastore, forgot_password_form=MyForgotPasswordForm, ) populate_data(app) client = app.test_client() # Test valid user but invalid additional form field # We should get a form error for the extra (invalid) field, no flash bad_data = dict(email="joe@lp.com", recaptcha="1234") good_data = dict(email="joe@lp.com", recaptcha="123456") with capture_flashes() as flashes: response = client.post("/reset", data=bad_data) assert b"Field must be at least 5" in response.data assert len(flashes) == 0 with capture_flashes() as flashes: response = client.post("/reset", data=good_data) assert len(flashes) == 1 # JSON with capture_flashes() as flashes: response = client.post("/reset", json=bad_data) assert response.status_code == 400 assert ( "Field must be at least 5" in response.json["response"]["field_errors"]["recaptcha"][0] ) assert len(flashes) == 0 with capture_flashes() as flashes: response = client.post("/reset", json=good_data) assert response.status_code == 200 assert len(flashes) == 0 # Try bad email AND bad recaptcha bad_data = dict(email="joe44-lp.com", recaptcha="1234") with capture_flashes() as flashes: response = client.post("/reset", data=bad_data) assert b"Field must be at least 5" in response.data assert len(flashes) == 0 with capture_flashes() as flashes: response = client.post("/reset", json=bad_data) assert response.status_code == 400 assert ( "Field must be at least 5" in response.json["response"]["field_errors"]["recaptcha"][0] ) assert len(response.json["response"]["errors"]) == 1 assert len(flashes) == 0 def _locked(self, form_error): if self.email == "gal@lp.com": form_error.append("You are not allowed to do that") return True return False @pytest.mark.app_settings(TESTING_USER_INJECT=dict(is_locked=_locked)) def test_override_user_locked(app, client, get_message): response = client.post("/reset", json=dict(email="gal@lp.com")) assert response.status_code == 400 assert response.json["response"]["errors"] == ["You are not allowed to do that"] assert response.json["response"]["field_errors"]["email"] == [ "You are not allowed to do that" ] @pytest.mark.filterwarnings("ignore") @pytest.mark.settings(auto_login_after_reset=True, post_reset_view="/post_reset") def test_auto_login(client, get_message): # test backwards compat flag (not OWASP recommended) with capture_reset_password_requests() as requests: response = client.post( "/reset", data=dict(email="joe@lp.com"), follow_redirects=True ) assert response.status_code == 200 token = requests[0]["token"] # Test submitting a new password with capture_flashes() as flashes: response = client.post( "/reset/" + token, data=dict(password="awesome sunset", password_confirm="awesome sunset"), follow_redirects=True, ) assert b"Post Reset" in response.data assert len(flashes) == 1 assert get_message("PASSWORD_RESET") == flashes[0]["message"].encode("utf-8") # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.filterwarnings("ignore") @pytest.mark.settings(auto_login_after_reset=True) def test_auto_login_json(client, get_message): # test backwards compat flag (not OWASP recommended) with capture_reset_password_requests() as requests: response = client.post( "/reset", json=dict(email="joe@lp.com"), ) assert response.headers["Content-Type"] == "application/json" assert response.status_code == 200 token = requests[0]["token"] # Test submitting a new password response = client.post( "/reset/" + token + "?include_auth_token", json=dict(password="awesome sunset", password_confirm="awesome sunset"), ) assert all( k in response.json["response"]["user"] for k in ["email", "authentication_token"] ) # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.flask_async() @pytest.mark.settings() def test_recoverable_json_async(app, client, get_message): recorded_resets = [] recorded_instructions_sent = [] @password_reset.connect_via(app) async def on_password_reset(myapp, user): recorded_resets.append(user) @reset_password_instructions_sent.connect_via(app) async def on_instructions_sent(myapp, **kwargs): recorded_instructions_sent.append(kwargs["user"]) # Test reset password creates a token and sends email with capture_reset_password_requests() as requests: response = client.post( "/reset", json=dict(email="joe@lp.com"), headers={"Content-Type": "application/json"}, ) assert len(recorded_instructions_sent) == 1 assert response.status_code == 200 token = requests[0]["token"] # Test submitting a new password response = client.post( "/reset/" + token + "?include_auth_token", json=dict(password="awesome sunset", password_confirm="awesome sunset"), ) assert not response.json["response"] assert len(recorded_resets) == 1 @pytest.mark.csrf() @pytest.mark.settings(post_reset_view="/post_reset_view") def test_csrf(app, client, get_message): response = client.get("/reset") csrf_token = get_form_input_value(response, "csrf_token") with capture_reset_password_requests() as requests: client.post( "/reset", data=dict(email="joe@lp.com", csrf_token=csrf_token), follow_redirects=True, ) token = requests[0]["token"] # use the token - no CSRF so shouldn't work data = {"password": "mypassword", "password_confirm": "mypassword"} response = client.post( "/reset/" + token, data=data, ) assert b"The CSRF token is missing" in response.data data["csrf_token"] = csrf_token response = client.post(f"/reset/{token}", data=data) assert check_location(app, response.location, "/post_reset_view") @pytest.mark.csrf(ignore_unauth=True) @pytest.mark.settings(post_reset_view="/post_reset_view") def test_auth_csrf(app, client, get_message): # Test reset when authenticated and unauth CSRF is off authenticate(client) response = client.get("/reset") csrf_token = get_form_input_value(response, "csrf_token") assert "matt@lp.com" == get_form_input_value(response, "email") with capture_reset_password_requests() as requests: client.post( "/reset", data=dict(email="matt@lp.com", csrf_token=csrf_token), follow_redirects=True, ) token = requests[0]["token"] # use the token - no CSRF so shouldn't work data = {"password": "mypassword", "password_confirm": "mypassword"} response = client.post( "/reset/" + token, data=data, ) assert b"The CSRF token is missing" in response.data data["csrf_token"] = csrf_token response = client.post(f"/reset/{token}", data=data) assert check_location(app, response.location, "/post_reset_view") assert not is_authenticated(client, get_message) def test_recoverable_auth_json(app, client, get_message, outbox): recorded_resets = [] recorded_instructions_sent = [] @password_reset.connect_via(app) def on_password_reset(app, user): recorded_resets.append(user) @reset_password_instructions_sent.connect_via(app) def on_instructions_sent(app, **kwargs): recorded_instructions_sent.append(kwargs["user"]) authenticate(client, "joe@lp.com", password="password") with capture_flashes() as flashes: # Test reset password creates a token and sends email with capture_reset_password_requests() as requests: response = client.post( "/reset", json=dict(email="joe@lp.com"), ) assert len(recorded_instructions_sent) == 1 assert len(outbox) == 1 assert response.status_code == 200 token = requests[0]["token"] # Test invalid email response = client.post( "/reset", json=dict(email="whoknows@lp.com"), ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "USER_DOES_NOT_EXIST" ) # Test submitting a new password response = client.post( "/reset/" + token + "?include_auth_token", json=dict(password="awesome sunset", password_confirm="awesome sunset"), ) assert not response.json["response"] assert len(recorded_resets) == 1 assert not is_authenticated(client, get_message) # Test logging in with the new password response = client.post( "/login?include_auth_token", json=dict(email="joe@lp.com", password="awesome sunset"), ) assert all( k in response.json["response"]["user"] for k in ["email", "authentication_token"] ) logout(client) # Use token again - should fail since already have set new password. response = client.post( "/reset/" + token, json=dict(password="newpassword", password_confirm="newpassword"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 400 assert len(recorded_resets) == 1 # Test invalid token response = client.post( "/reset/bogus", json=dict(password="newpassword", password_confirm="newpassword"), headers={"Content-Type": "application/json"}, ) assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_RESET_PASSWORD_TOKEN" ) assert len(flashes) == 0 @pytest.mark.username_recovery() def test_username_recovery_valid_email(app, clients, get_message, outbox, signals): # Test the username recovery view response = clients.get("/recover-username") assert b"

    Username Recovery

    " in response.data response = clients.post( "/recover-username", data=dict(email="joe@lp.com"), follow_redirects=True ) check_signals(signals, "username_recovery_email_sent") assert signals["username_recovery_email_sent"][0]["user_email"] == "joe@lp.com" assert len(outbox) == 1 assert response.status_code == 200 with capture_flashes() as flashes: response = clients.post( "/recover-username", data=dict(email="joe@lp.com"), follow_redirects=True, ) assert len(flashes) == 1 assert get_message("USERNAME_RECOVERY_REQUEST") == flashes[0]["message"].encode( "utf-8" ) # Validate the emailed username email = outbox[1] assert "Your username is: joe" in email.body # Test JSON responses response = clients.post( "/recover-username", json=dict(email="joe@lp.com"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 assert response.headers["Content-Type"] == "application/json" @pytest.mark.username_recovery() def test_username_recovery_invalid_email(app, client, outbox): response = client.post( "/recover-username", data=dict(email="bogus@lp.com"), follow_redirects=True ) assert len(outbox) == 0 assert response.status_code == 200 # Test JSON responses response = client.post( "/recover-username", json=dict(email="bogus@lp.com"), ) assert response.status_code == 400 assert response.headers["Content-Type"] == "application/json" assert len(response.json["response"]["errors"]) == 1 assert ( "Specified user does not exist" in response.json["response"]["field_errors"]["email"][0] ) @pytest.mark.username_recovery() @pytest.mark.settings(return_generic_responses=True) def test_username_recovery_generic_responses( app, clients, get_message, outbox, signals ): # Test with valid email with capture_flashes() as flashes: response = clients.post( "/recover-username", data=dict(email="joe@lp.com"), follow_redirects=True, ) assert len(flashes) == 1 assert get_message("USERNAME_RECOVERY_REQUEST") == flashes[0]["message"].encode( "utf-8" ) check_signals(signals, "username_recovery_email_sent") assert signals["username_recovery_email_sent"][0]["user_email"] == "joe@lp.com" assert len(outbox) == 1 assert response.status_code == 200 # Test with non-existant email (should still return 200) with capture_flashes() as flashes: response = clients.post( "/recover-username", data=dict(email="bogus@lp.com"), follow_redirects=True, ) assert len(flashes) == 1 assert get_message("USERNAME_RECOVERY_REQUEST") == flashes[0]["message"].encode( "utf-8" ) # Validate no email was sent (there should only be one from the previous test) check_signals(signals, "username_recovery_email_sent") assert len(outbox) == 1 assert response.status_code == 200 # Test JSON responses - valid email response = clients.post("/recover-username", json=dict(email="joe@lp.com")) assert response.status_code == 200 assert response.headers["Content-Type"] == "application/json" # Test JSON responses - invalid email response = clients.post("/recover-username", json=dict(email="bogus@lp.com")) assert response.status_code == 200 assert response.headers["Content-Type"] == "application/json" assert not any(e in response.json["response"].keys() for e in ["error", "errors"]) @pytest.mark.username_recovery() def test_ur_inactive(client, get_message): response = client.post( "/recover-username", data=dict(email="tiya@lp.com"), follow_redirects=True ) assert get_message("DISABLED_ACCOUNT") in response.data @pytest.mark.username_recovery() @pytest.mark.confirmable() def test_ur_confirm_needed(app, client, get_message, outbox): response = client.post("/recover-username", json=dict(email="joe@lp.com")) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "CONFIRMATION_REQUIRED" ) assert len(outbox) == 0 @pytest.mark.username_recovery() @pytest.mark.confirmable() @pytest.mark.settings(requires_confirmation_error_view="/confirm") def test_ur_confirmation_error_redirect(app, client): response = client.post( "/recover-username", data=dict(email="joe@lp.com"), follow_redirects=True ) assert b"send_confirmation_form" in response.data assert b"joe@lp.com" in response.data @pytest.mark.username_recovery() @pytest.mark.app_settings(TESTING_USER_INJECT=dict(is_locked=_locked)) def test_ur_override_user_locked(app, client, get_message): response = client.post("/recover-username", json=dict(email="gal@lp.com")) assert response.status_code == 400 assert response.json["response"]["errors"] == ["You are not allowed to do that"] assert response.json["response"]["field_errors"]["email"] == [ "You are not allowed to do that" ] pallets-eco-flask-security-72bf929/tests/test_recovery_codes.py000066400000000000000000000304211525721743500250340ustar00rootroot00000000000000""" test_recovery_codes ~~~~~~~~~~~~~~~~~ recovery code tests :copyright: (c) 2022-2024 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import re import pytest from tests.test_two_factor import tf_authenticate from tests.test_utils import ( authenticate, logout, reset_fresh, setup_tf_sms, ) pytestmark = pytest.mark.two_factor() @pytest.mark.settings(multi_factor_recovery_codes=True) def test_rc_json(app, clients, get_message): # Test recovery codes # gal has two-factor already setup for 'sms' client = clients headers = {"Accept": "application/json", "Content-Type": "application/json"} tf_authenticate(app, client) response = client.get("/mf-recovery-codes", headers=headers) assert response.status_code == 200 codes = response.json["response"]["recovery_codes"] assert len(codes) == 0 response = client.post("/mf-recovery-codes", headers=headers) codes = response.json["response"]["recovery_codes"] assert len(codes) == 5 response = client.get("/mf-recovery-codes", headers=headers) recodes = response.json["response"]["recovery_codes"] assert len(recodes) == 5 and codes[0] == recodes[0] response = client.get("/mf-recovery-codes", headers=headers) assert response.status_code == 200 assert not hasattr(response.json["response"], "recovery_codes") # endpoint is for unauthenticated only response = client.post("/mf-recovery", json=dict(code=codes[0])) assert response.status_code == 400 logout(client) response = client.post("/login", json=dict(email="gal@lp.com", password="password")) assert response.json["response"]["tf_required"] response = client.post("/mf-recovery", json=dict(code=codes[0])) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 # logout and try again - first code shouldn't work again logout(client) response = client.post("/login", json=dict(email="gal@lp.com", password="password")) assert response.json["response"]["tf_required"] response = client.post("/mf-recovery", json=dict(code=codes[0])) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_RECOVERY_CODE" ) response = client.post("/mf-recovery", json=dict(code=codes[1])) assert response.status_code == 200 @pytest.mark.settings(multi_factor_recovery_codes=True) def test_rc(app, client, get_message): # Test recovery codes # gal has two-factor already setup for 'sms' tf_authenticate(app, client) response = client.get("/mf-recovery-codes?show_codes=hi") assert response.status_code == 200 assert get_message("NO_RECOVERY_CODES_SETUP") in response.data response = client.post("/mf-recovery-codes") rd = response.data.decode("utf-8") codes = re.findall(r"[a-f,\d]{4}-[a-f,\d]{4}-[a-f,\d]{4}", rd) assert len(codes) == 5 response = client.get("/mf-recovery-codes?show_codes=hi") assert response.status_code == 200 assert b"Recovery Codes" in response.data rd = response.data.decode("utf-8") codes = re.findall(r"[a-f,\d]{4}-[a-f,\d]{4}-[a-f,\d]{4}", rd) assert len(codes) == 5 # endpoint is for unauthenticated only response = client.post( "/mf-recovery", data=dict(code=codes[0]), follow_redirects=False ) assert response.status_code in [302, 303] logout(client) response = client.post("/login", json=dict(email="gal@lp.com", password="password")) assert response.json["response"]["tf_required"] response = client.post( "/mf-recovery", data=dict(code=codes[0]), follow_redirects=True ) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 # logout and try again - first code shouldn't work again logout(client) response = client.post("/login", data=dict(email="gal@lp.com", password="password")) response = client.post("/mf-recovery", data=dict(code=codes[0])) assert get_message("INVALID_RECOVERY_CODE") in response.data response = client.post( "/mf-recovery", data=dict(code=codes[1]), follow_redirects=True ) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.settings(multi_factor_recovery_codes=True) def test_rc_reset(app, client, get_message): # test that reset_user_access, removes recovery codes headers = {"Accept": "application/json", "Content-Type": "application/json"} tf_authenticate(app, client) response = client.post("/mf-recovery-codes", headers=headers) codes = response.json["response"]["recovery_codes"] assert len(codes) == 5 with app.app_context(): user = app.security.datastore.find_user(email="gal@lp.com") app.security.datastore.reset_user_access(user) app.security.datastore.commit() client.post("/login", json=dict(email="gal@lp.com", password="password")) response = client.get("/mf-recovery-codes", headers=headers) codes = response.json["response"]["recovery_codes"] assert len(codes) == 0 @pytest.mark.settings(multi_factor_recovery_codes=True, url_prefix="/api") def test_rc_bad_state(app, client, get_message): response = client.post("/api/mf-recovery", json=dict(code="hi")) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "TWO_FACTOR_PERMISSION_DENIED" ) response = client.post( "/api/mf-recovery", data=dict(code="hi"), follow_redirects=False ) assert response.status_code in [302, 303] assert "/api/login" in response.location @pytest.mark.settings(multi_factor_recovery_codes=True) def test_rc_rescue(app, client, get_message): headers = {"Accept": "application/json", "Content-Type": "application/json"} tf_authenticate(app, client) response = client.post("/mf-recovery-codes", headers=headers) codes = response.json["response"]["recovery_codes"] assert len(codes) == 5 logout(client) data = dict(email="gal@lp.com", password="password") response = client.post("/login", json=data) assert response.json["response"]["tf_required"] response = client.get("/tf-rescue") assert b"Use previously downloaded recovery code" in response.data response = client.get("/tf-rescue", headers=headers) options = response.json["response"]["recovery_options"] assert "recovery_code" in options.keys() assert "/mf-recovery" in options["recovery_code"] response = client.post( "/tf-rescue", data=dict(help_setup="recovery_code"), follow_redirects=False ) assert "/mf-recovery" in response.location @pytest.mark.settings(multi_factor_recovery_codes=True) def test_fresh(app, client): # Make sure fetching recovery codes is protected with a freshness check. headers = {"Accept": "application/json", "Content-Type": "application/json"} authenticate(client) response = client.post("/mf-recovery-codes", headers=headers) codes = response.json["response"]["recovery_codes"] assert len(codes) == 5 reset_fresh(client, app.config["SECURITY_FRESHNESS"]) response = client.post("/mf-recovery-codes", headers=headers) assert response.status_code == 401 assert response.json["response"]["reauth_required"] response = client.post("/verify", json=dict(password="password")) assert response.status_code == 200 response = client.post("/mf-recovery-codes", headers=headers) codes = response.json["response"]["recovery_codes"] assert len(codes) == 5 @pytest.mark.settings( multi_factor_recovery_codes=True, multi_factor_recovery_codes_keys=[b"6_WhDTwI1RKJ3_ra9nADCBQbrRywlA88psGsq21xcsU="], ) def test_rc_json_encrypted(app, client, get_message): # Test recovery codes with encryption option # gal has two-factor already setup for 'sms' headers = {"Accept": "application/json", "Content-Type": "application/json"} tf_authenticate(app, client) response = client.post("/mf-recovery-codes", headers=headers) codes = response.json["response"]["recovery_codes"] assert len(codes) == 5 # endpoint is for unauthenticated only response = client.post("/mf-recovery", json=dict(code=codes[0])) assert response.status_code == 400 logout(client) response = client.post("/login", json=dict(email="gal@lp.com", password="password")) assert response.json["response"]["tf_required"] response = client.post("/mf-recovery", json=dict(code=codes[0])) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 # logout and try again - first code shouldn't work again logout(client) response = client.post("/login", json=dict(email="gal@lp.com", password="password")) assert response.json["response"]["tf_required"] response = client.post("/mf-recovery", json=dict(code=codes[0])) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_RECOVERY_CODE" ) response = client.post("/mf-recovery", json=dict(code=codes[1])) assert response.status_code == 200 # make sure DB actually has encrypted codes! with app.test_request_context("/"): user = app.security.datastore.find_user(email="gal@lp.com") codes = user.mf_recovery_codes assert len(codes) == 3 assert len(codes[0]) == 100 @pytest.mark.settings( multi_factor_recovery_codes=True, multi_factor_recovery_codes_keys=[b"6_WhDTwI1RKJ3_ra9nADCBQbrRywlA88psGsq21xcsU="], ) def test_rc_json_encrypted_multi(app, client, get_message): # Test recovery codes with encryption option and multiple keys # gal has two-factor already setup for 'sms' from cryptography.fernet import Fernet headers = {"Accept": "application/json", "Content-Type": "application/json"} tf_authenticate(app, client) response = client.post("/mf-recovery-codes", headers=headers) codes = response.json["response"]["recovery_codes"] assert len(codes) == 5 logout(client) response = client.post("/login", json=dict(email="gal@lp.com", password="password")) assert response.json["response"]["tf_required"] response = client.post("/mf-recovery", json=dict(code=codes[0])) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 logout(client) # add a new key to cryptor and verify old code still works key2 = Fernet.generate_key() newkeys = [key2, b"6_WhDTwI1RKJ3_ra9nADCBQbrRywlA88psGsq21xcsU="] app.security.mf_recovery_codes_util.setup_cryptor(newkeys) response = client.post("/login", json=dict(email="gal@lp.com", password="password")) assert response.json["response"]["tf_required"] response = client.post("/mf-recovery", json=dict(code=codes[1])) assert response.status_code == 200 logout(client) # creat codes for new user - this should use the new primary key. response = client.post( "/login", json=dict(email="matt@lp.com", password="password") ) setup_tf_sms(client) response = client.post("/mf-recovery-codes", headers=headers) matt_codes = response.json["response"]["recovery_codes"] logout(client) # Now remove older key and codes for gal@lp.com shouldn't work app.security._mf_recovery_codes_util.setup_cryptor([key2]) response = client.post("/login", json=dict(email="gal@lp.com", password="password")) assert response.json["response"]["tf_required"] response = client.post("/mf-recovery", json=dict(code=codes[2])) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_RECOVERY_CODE" ) # matts codes should work response = client.post( "/login", json=dict(email="matt@lp.com", password="password") ) assert response.json["response"]["tf_required"] response = client.post("/mf-recovery", json=dict(code=matt_codes[0])) assert response.status_code == 200 pallets-eco-flask-security-72bf929/tests/test_registerable.py000066400000000000000000001261211525721743500244740ustar00rootroot00000000000000""" test_registerable ~~~~~~~~~~~~~~~~~ Registerable tests """ import pytest import re from flask import Flask from tests.conftest import v2_param from tests.test_utils import ( authenticate, get_form_input_value, get_form_input, init_app_with_options, json_authenticate, logout, is_authenticated, ) from flask_security import Security, UserMixin, user_registered, user_not_registered from flask_security.forms import ( ConfirmRegisterForm, RegisterForm, RegisterFormV2, StringField, _default_field_labels, ) from flask_security.utils import localize_callback pytestmark = pytest.mark.registerable() @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.settings(post_register_view="/post_register") def test_registerable_flag(clients, app, get_message, outbox): recorded = [] # Test the register view response = clients.get("/register") assert b"

    Register

    " in response.data assert re.search(b']*type="email"[^>]*>', response.data) assert get_form_input_value(response, "email") is not None assert not get_form_input_value(response, "username") # Test registering is successful, sends email, and fires signal @user_registered.connect_via(app) def on_user_registered(app, **kwargs): assert isinstance(app, Flask) assert isinstance(kwargs["user"], UserMixin) assert kwargs["confirm_token"] is None assert len(kwargs["form_data"].keys()) > 0 recorded.append(kwargs["user"]) data = dict( email="dude@lp.com", password="battery staple", password_confirm="battery staple", next="", ) response = clients.post("/register", data=data, follow_redirects=True) assert len(recorded) == 1 assert len(outbox) == 1 assert b"Post Register" in response.data logout(clients) # Test user can login after registering response = authenticate(clients, email="dude@lp.com", password="battery staple") assert response.status_code in [302, 303] logout(clients) # Test registering with an existing email data = dict( email="dude@lp.com", password="password", password_confirm="password", next="" ) response = clients.post("/register", data=data, follow_redirects=True) assert get_message("EMAIL_ALREADY_ASSOCIATED", email="dude@lp.com") in response.data # Test registering with an existing email but case insensitive data = dict( email="Dude@lp.com", password="password", password_confirm="password", next="" ) response = clients.post("/register", data=data, follow_redirects=True) assert get_message("EMAIL_ALREADY_ASSOCIATED", email="Dude@lp.com") in response.data # Test registering with JSON data = dict( email="dude2@lp.com", password="horse battery", password_confirm="horse battery" ) response = clients.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.headers["content-type"] == "application/json" assert response.json["meta"]["code"] == 200 assert len(response.json["response"]) == 3 assert all( k in response.json["response"] for k in ["csrf_token", "user", "tf_required"] ) logout(clients) # Test registering with invalid JSON data = dict(email="bogus", password="password", password_confirm="password") response = clients.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.headers["content-type"] == "application/json" assert response.json["meta"]["code"] == 400 logout(clients) # Test ?next param data = dict( email="dude3@lp.com", password="horse staple", password_confirm="horse staple", next="", ) response = clients.post("/register?next=/page1", data=data, follow_redirects=True) assert b"Page 1" in response.data @pytest.mark.csrf(csrfprotect=True) def test_form_csrf(app, client): # Test that CSRFprotect config catches CSRF requirement at unauth_csrf() decorator. # Note that in this case - 400 is returned - though if CSRFprotect isn't enabled # then CSRF errors are caught at form.submit time and a 200 with error values # is returned. response = client.post( "/register", data=dict( email="csrf@example.com", password="mypassword", password_confirm="mypassword", ), ) assert response.status_code == 400 assert b"The CSRF token is missing" in response.data response = client.get("/register") csrf_token = get_form_input_value(response, "csrf_token") response = client.post( "/register", data=dict( email="csrf@example.com", password="mypassword", password_confirm="mypassword", csrf_token=csrf_token, ), follow_redirects=False, ) assert response.status_code in [302, 303] assert response.location == "/" @pytest.mark.confirmable() @pytest.mark.babel(babel_default_locale="cic_US", test_xlations=True) def test_xlation(app, client, get_message_local, outbox): # Test form and email translation confirmation_token = [] @user_registered.connect_via(app) def on_user_registered(app, **kwargs): confirmation_token.append(kwargs["confirmation_token"]) response = client.get("/register", follow_redirects=True) with app.test_request_context(): # Check header header = localize_callback("Register") assert "OKAY!" in header # ensures actual translation assert f"

    {header}

    ".encode() in response.data submit = localize_callback(_default_field_labels["register"]) assert "OKAY!" in submit assert f'value="{submit}"'.encode() in response.data response = client.post( "/register", data={ "email": "me@fr.com", "password": "new strong password", "password_confirm": "new strong password", }, follow_redirects=True, ) with app.test_request_context(): flash = get_message_local("CONFIRM_REGISTRATION", email="me@fr.com") assert "OKAY!" in flash assert flash.encode() in response.data assert b"Home Page" in response.data assert len(outbox) == 1 subj = localize_callback(app.config["SECURITY_EMAIL_SUBJECT_REGISTER"]) assert "OKAY!" in subj assert subj in outbox[0].subject lc = localize_callback( 'Use this link to confirm your email' " address.", confirmation_link=f"http://localhost/confirm/{confirmation_token[0]}", ) assert "OKAY!" in lc assert lc in outbox[0].alts["html"] @pytest.mark.confirmable() @pytest.mark.settings(use_register_v2=False) def test_required_password(client, get_message): # when confirm required - should not require confirm_password - but should # require a password data = dict(email="trp@lp.com", password="") response = client.post("/register", data=data, follow_redirects=True) assert get_message("PASSWORD_NOT_PROVIDED") in response.data data = dict(email="trp@lp.com", password="battery staple") response = client.post("/register", data=data, follow_redirects=True) assert get_message("CONFIRM_REGISTRATION", email="trp@lp.com") in response.data def test_register_non_string_password(client): # ConfirmRegisterForm and RegisterFormV2 have special validation code for # OWASP (user enumeration). So they should have a separate test case from # general IsString tests. response = client.post( "/register", json=dict(email="newuser@lp.com", password={"not": "a string"}), ) assert response.status_code == 400 @pytest.mark.settings(use_register_v2=False) def test_required_password_confirm(client, get_message): response = client.post( "/register", data={ "email": "trp@lp.com", "password": "password", "password_confirm": "notpassword", }, follow_redirects=True, ) assert get_message("RETYPE_PASSWORD_MISMATCH") in response.data response = client.post( "/register", data={"email": "trp@lp.com", "password": "password", "password_confirm": ""}, follow_redirects=True, ) assert get_message("PASSWORD_NOT_PROVIDED") in response.data @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.unified_signin() @pytest.mark.confirmable() @pytest.mark.settings(password_required=False) def test_allow_null_password(app, client, get_message): # Test password not required with either register form response = client.get("/register") data = dict(email="trp@lp.com") pw = get_form_input(response, "password") assert "required" not in pw pwc = get_form_input(response, "password_confirm") assert not pwc or "required" not in pwc response = client.post("/register", data=data, follow_redirects=True) assert get_message("CONFIRM_REGISTRATION", email="trp@lp.com") in response.data logout(client) # should be able to register with password and password_confirm (with RegisterFormV2 if app.config["SECURITY_USE_REGISTER_V2"]: data = dict( email="trp2@lp.com", password="battery staple", password_confirm="battery staples", ) response = client.post("/register", data=data, follow_redirects=True) assert get_message("RETYPE_PASSWORD_MISMATCH") in response.data data = dict( email="trp2@lp.com", password="battery staple", password_confirm="battery staple", ) response = client.post("/register", data=data, follow_redirects=True) assert get_message("CONFIRM_REGISTRATION", email="trp2@lp.com") in response.data @pytest.mark.unified_signin() @pytest.mark.settings(password_required=False, use_register_v2=False) def test_allow_null_password_nologin(client, get_message): # If unified sign in is enabled - should be able to register w/o password # With confirmable false - should be logged in automatically upon register. # But shouldn't be able to perform normal login again data = dict(email="trp@lp.com", password="") response = client.post("/register", data=data, follow_redirects=True) assert b"Welcome trp@lp.com" in response.data logout(client) # Make sure can't log in response = authenticate(client, email="trp@lp.com", password="") assert get_message("PASSWORD_NOT_PROVIDED") in response.data response = authenticate(client, email="trp@lp.com", password="NoPassword") assert get_message("INVALID_PASSWORD") in response.data @pytest.mark.settings( register_url="/custom_register", post_register_view="/post_register" ) def test_custom_register_url(client): response = client.get("/custom_register") assert b"

    Register

    " in response.data data = dict( email="dude@lp.com", password="battery staple", password_confirm="battery staple", next="", ) response = client.post("/custom_register", data=data, follow_redirects=True) assert b"Post Register" in response.data @pytest.mark.settings(register_user_template="custom_security/register_user.html") def test_custom_register_template(client): response = client.get("/register") assert b"CUSTOM REGISTER USER" in response.data @pytest.mark.settings(send_register_email=False) def test_disable_register_emails(client, app, outbox): data = dict( email="dude@lp.com", password="password", password_confirm="password", next="" ) client.post("/register", data=data, follow_redirects=True) assert len(outbox) == 0 @pytest.mark.two_factor() @pytest.mark.settings(two_factor_required=True) def test_two_factor(app, client): """If two-factor is enabled, the register shouldn't login, but start the 2-factor setup. """ data = dict(email="dude@lp.com", password="password", password_confirm="password") response = client.post("/register", data=data, follow_redirects=False) assert "tf-setup" in response.location # make sure not logged in response = client.get("/profile") assert response.status_code in [302, 303] assert response.location == "/login?next=/profile" @pytest.mark.two_factor() @pytest.mark.settings(two_factor_required=True) def test_two_factor_json(app, client, get_message): data = dict(email="dude@lp.com", password="password", password_confirm="password") response = client.post("/register", content_type="application/json", json=data) assert response.status_code == 200 assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "setup_from_login" # make sure not logged in response = client.get("/profile", headers={"accept": "application/json"}) assert response.status_code == 401 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "UNAUTHENTICATED" ) def test_form_data_is_passed_to_user_registered_signal(app, sqlalchemy_datastore): class MyRegisterForm(RegisterFormV2): additional_field = StringField("additional_field") app.security = Security( app, datastore=sqlalchemy_datastore, register_form=MyRegisterForm ) recorded = [] @user_registered.connect_via(app) def on_user_registered(app, **kwargs): assert isinstance(app, Flask) assert isinstance(kwargs["user"], UserMixin) assert kwargs["confirm_token"] is None assert kwargs["confirmation_token"] is None assert kwargs["form_data"]["additional_field"] == "additional_data" recorded.append(kwargs["user"]) client = app.test_client() data = dict( email="dude@lp.com", password="password", password_confirm="password", additional_field="additional_data", ) response = client.post("/register", data=data, follow_redirects=True) assert response.status_code == 200 assert len(recorded) == 1 @pytest.mark.settings(password_complexity_checker="zxcvbn") @pytest.mark.filterwarnings("ignore:.*The ConfirmRegisterForm.*:DeprecationWarning") def test_easy_password(app, sqlalchemy_datastore): class MyRegisterForm(ConfirmRegisterForm): username = StringField("Username") app.config["SECURITY_CONFIRM_REGISTER_FORM"] = MyRegisterForm security = Security(datastore=sqlalchemy_datastore) security.init_app(app) client = app.test_client() # With zxcvbn data = dict( email="dude@lp.com", username="dude", password="mattmatt2", password_confirm="mattmatt2", ) response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.headers["Content-Type"] == "application/json" assert response.status_code == 400 # Response from zxcvbn assert "Repeats like" in response.json["response"]["errors"][0] # Test that username affects password selection data = dict( email="dude@lp.com", username="Joe", password="JoeTheDude", password_confirm="JoeTheDude", ) response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.headers["Content-Type"] == "application/json" assert response.status_code == 400 # Response from zxcvbn assert ( "Password not complex enough" in response.json["response"]["field_errors"]["password"][0] ) @pytest.mark.settings(username_enable=True, password_confirm_required=False) def test_nullable_username(app, client): # sqlalchemy datastore uses fsqlav2 which has username as unique and nullable # make sure can register multiple users with no username # Note that current WTForms (2.2.1) has a bug where StringFields can never be # None - it changes them to an empty string. DBs don't like that if you have # your column be 'nullable'. data = dict(email="u1@test.com", password="password") response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 200 logout(client) data = dict(email="u2@test.com", password="password") response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 200 def test_email_normalization(app, client): # should be able to login either as LP.com or lp.com or canonical unicode form data = dict( email="Imnumber\N{OHM SIGN}@LP.com", password="battery staple", password_confirm="battery staple", ) response = client.post("/register", data=data, follow_redirects=True) assert b"Home Page" in response.data logout(client) # Test user can login after registering response = authenticate( client, email="Imnumber\N{OHM SIGN}@lp.com", password="battery staple" ) assert response.status_code in [302, 303] logout(client) # Test user can login after registering using original non-canonical email response = authenticate( client, email="Imnumber\N{OHM SIGN}@LP.com", password="battery staple" ) assert response.status_code in [302, 303] logout(client) # Test user can login after registering using original non-canonical email response = authenticate( client, email="Imnumber\N{GREEK CAPITAL LETTER OMEGA}@LP.com", password="battery staple", ) assert response.status_code in [302, 303] def test_email_normalization_options(app, client, get_message): # verify can set options for email_validator data = dict( email="\N{BLACK SCISSORS}@LP.com", password="battery staple", password_confirm="battery staple", ) response = client.post("/register", data=data, follow_redirects=True) assert b"Home Page" in response.data logout(client) # turn off allowing 'local' part unicode. app.config["SECURITY_EMAIL_VALIDATOR_ARGS"] = {"allow_smtputf8": False} data = dict( email="\N{WHITE SCISSORS}@LP.com", password="battery staple", password_confirm="battery staple", ) response = client.post("/register", data=data, follow_redirects=True) assert response.status_code == 200 assert get_message("INVALID_EMAIL_ADDRESS") in response.data @pytest.mark.babel() def test_form_error(app, client, get_message): # A few form validations use ValidatorMixin which provides a lazy string # Since CLI doesn't render_template it was seeing those lazy strings. # This test basically just illustrates all this. from babel.support import LazyProxy with app.test_request_context("/register"): rform = ConfirmRegisterForm() rform.validate() assert isinstance(rform.errors["email"][0], LazyProxy) assert str(rform.errors["email"][0]).encode("utf-8") == get_message( "EMAIL_NOT_PROVIDED" ) # make sure rendered template has converted LocalProxy strings. rendered = app.security.render_template( app.config["SECURITY_REGISTER_USER_TEMPLATE"], register_user_form=rform, ) assert get_message("EMAIL_NOT_PROVIDED") in rendered.encode("utf-8") @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.settings(username_enable=True, password_confirm_required=False) @pytest.mark.unified_signin() def test_username(app, clients, get_message): client = clients data = dict( email="dude@lp.com", username="dude", password="awesome sunset", ) response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.headers["Content-Type"] == "application/json" assert response.status_code == 200 logout(client) # login using historic - email field to hold a username - won't work since # it is an EmailField response = json_authenticate(client, email="dude", password="awesome sunset") assert response.status_code == 400 assert ( get_message("INVALID_EMAIL_ADDRESS", username="dude") == response.json["response"]["errors"][0].encode() ) # login using username response = client.post( "/login", json=dict(username="dude", password="awesome sunset") ) assert response.status_code == 200 assert is_authenticated(client, get_message) logout(client) # login with email response = client.post( "/login", json=dict(email="dude@lp.com", password="awesome sunset") ) assert response.status_code == 200 assert is_authenticated(client, get_message) logout(client) response = client.post( "/login", json=dict(emails="dude", password="awesome sunset") ) assert response.status_code == 400 assert ( get_message("USER_DOES_NOT_EXIST") == response.json["response"]["field_errors"][""][0].encode() ) # login using us-signin response = client.post( "/us-signin", data=dict(identity="dude", passcode="awesome sunset"), follow_redirects=True, ) assert b"Welcome dude@lp.com" in response.data # make sure username is unique logout(client) data = dict( email="dude@lp.com", username="dude", password="awesome sunset", ) response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert ( get_message("USERNAME_ALREADY_ASSOCIATED", username="dude") == response.json["response"]["field_errors"]["username"][0].encode() ) @pytest.mark.settings(username_enable=True) def test_username_not_set(app, client, get_message): # login with null username - shouldn't match # note that this is caught at LoginForm - it doesn't force a DB call response = client.post( "/register", json=dict( email="justemail@lp.com", username="", password="awesome sunset", password_confirm="awesome sunset", ), ) assert response.status_code == 200 client.post("/logout") response = client.post("/login", json=dict(username="", password="awesome sunset")) assert response.status_code == 400 assert ( get_message("USER_DOES_NOT_EXIST") == response.json["response"]["field_errors"][""][0].encode() ) @pytest.mark.settings(username_enable=True) def test_username_template(app, client): # verify template displays username option response = client.get("/register") username_field = get_form_input_value(response, "username") assert username_field is not None @pytest.mark.settings(username_enable=True) @pytest.mark.unified_signin() def test_username_normalize(app, client, get_message): data = dict( email="dude@lp.com", username="Imnumber\N{ROMAN NUMERAL ONE}", password="awesome sunset", password_confirm="awesome sunset", ) response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 200 logout(client) response = client.post( "/us-signin", data=dict( identity="Imnumber\N{LATIN CAPITAL LETTER I}", passcode="awesome sunset" ), follow_redirects=True, ) assert b"Welcome dude@lp.com" in response.data @pytest.mark.settings(username_enable=True, username_required=True) def test_username_errors(app, client, get_message): data = dict( email="dude@lp.com", username="dud", password="awesome sunset", password_confirm="awesome sunset", ) response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert "Username must be at least" in response.json["response"]["errors"][0] data["username"] = "howlongcanIbebeforeyoucomplainIsupposereallyreallylong" response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert "Username must be at least" in response.json["response"]["errors"][0] # try evil characters data["username"] = "hi " response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert ( get_message("INVALID_INPUT") == response.json["response"]["field_errors"]["username"][0].encode() ) # No spaces or punctuation data["username"] = "hi there?" response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert ( get_message("USERNAME_DISALLOWED_CHARACTERS") in response.json["response"]["errors"][0].encode() ) data["username"] = None response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert ( get_message("USERNAME_NOT_PROVIDED") == response.json["response"]["errors"][0].encode() ) @pytest.mark.settings( username_enable=True, username_required=True, username_allowed_chars=None ) def test_username_anything_goes(app, client, get_message): # allow app to allow any characters in a username data = dict( email="dude@lp.com", username="hi there?", password="awesome sunset", password_confirm="awesome sunset", ) response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 200 def test_username_not_enabled(app, client, get_message): response = client.get("/register") assert b"username" not in response.data assert not hasattr(RegisterForm, "username") @pytest.mark.filterwarnings( "ignore:.*The RegisterForm is deprecated.*:DeprecationWarning" ) def test_legacy_style_login(app, sqlalchemy_datastore, get_message): # Show how to setup LoginForm to mimic legacy behavior of # allowing any identity in the email field. # N.B. for simplicity we don't enable confirmable.... from flask_security import ( RegisterForm, LoginForm, Security, uia_username_mapper, unique_identity_attribute, ) from flask_security.utils import lookup_identity from werkzeug.local import LocalProxy from wtforms import StringField, ValidationError, validators def username_validator(form, field): # Side-effect - field.data is updated to normalized value. # Use proxy to we can declare this prior to initializing Security. _security = LocalProxy(lambda: app.extensions["security"]) msg, field.data = _security.username_util.validate(field.data) if msg: raise ValidationError(msg) class MyRegisterForm(RegisterForm): # Note that unique_identity_attribute uses the defined field 'mapper' to # normalize. We validate before that to give better error messages and # to set the normalized value into the form for saving. username = StringField( "Username", validators=[ validators.data_required(), username_validator, unique_identity_attribute, ], ) class MyLoginForm(LoginForm): email = StringField("email", [validators.data_required()]) def validate(self, **kwargs): self.user = lookup_identity(self.email.data) self.ifield = self.email if not super().validate(**kwargs): return False return True app.config["SECURITY_USER_IDENTITY_ATTRIBUTES"] = [ {"username": {"mapper": uia_username_mapper}} ] security = Security( datastore=sqlalchemy_datastore, register_form=MyRegisterForm, login_form=MyLoginForm, ) security.init_app(app) client = app.test_client() data = dict( email="mary2@lp.com", username="mary", password="awesome sunset", password_confirm="awesome sunset", ) response = client.post("/register", data=data, follow_redirects=True) assert b"Welcome mary" in response.data logout(client) # log in with username response = client.post( "/login", data=dict(email="mary", password="awesome sunset"), follow_redirects=True, ) # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.confirmable() @pytest.mark.settings( return_generic_responses=True, username_enable=True, password_confirm_required=False ) def test_generic_response(app, client, get_message, outbox): # Register should not expose whether email/username is already in system. recorded = [] @user_not_registered.connect_via(app) def on_user_not_registered(app, **kwargs): recorded.append(kwargs) # register new user data = dict( email="dude@lp.com", username="dude", password="awesome sunset", ) response = client.post("/register", json=data) assert response.status_code == 200 assert len(outbox) == 1 assert len(recorded) == 0 # try again - should not get ANY error - but should get an email response = client.post("/register", json=data) assert response.status_code == 200 assert not any( e in response.json["response"].keys() for e in ["errors", "field_errors"] ) # this is using the test template matcher = re.findall(r"\w+:.*", outbox[1].body, re.IGNORECASE) kv = {m.split(":")[0]: m.split(":", 1)[1] for m in matcher} assert kv["User"] == "dude" assert kv["Email"] == "dude@lp.com" assert kv["RegisterBlueprint"] == "True" assert "/confirm" in kv["ConfirmationLink"] assert kv["ConfirmationToken"] assert not kv["ResetLink"] assert not kv["ResetToken"] # verify that signal sent. assert len(recorded) == 1 nr = recorded[0] assert nr["existing_email"] assert nr["user"].email == "dude@lp.com" assert nr["form_data"]["email"] == "dude@lp.com" # Forms should get generic response - even though email already registered. response = client.post("/register", data=data, follow_redirects=True) assert get_message("CONFIRM_REGISTRATION", email="dude@lp.com") in response.data assert len(outbox) == 3 assert len(recorded) == 2 # Try same email with different username response = client.post( "/register", data=dict(email="dude@lp.com", username="dude2", password="awesome sunset"), follow_redirects=True, ) assert get_message("CONFIRM_REGISTRATION", email="dude@lp.com") in response.data assert len(outbox) == 4 assert len(recorded) == 3 matcher = re.findall(r"\w+:.*", outbox[3].body, re.IGNORECASE) kv = {m.split(":")[0]: m.split(":", 1)[1] for m in matcher} assert kv["User"] == "dude" @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.confirmable() @pytest.mark.settings( return_generic_responses=True, username_enable=True, password_confirm_required=False ) def test_gr_existing_username(app, client, get_message, outbox): # Test a new email with an existing username # Should still return errors such as illegal password recorded = [] @user_not_registered.connect_via(app) def on_user_not_registered(app, **kwargs): recorded.append(kwargs) client.post( "/register", json=dict(email="dude@lp.com", username="dude", password="awesome sunset"), ) response = client.post( "/register", data=dict(email="dude39@lp.com", username="dude", password="awesome sunset"), follow_redirects=True, ) assert get_message("CONFIRM_REGISTRATION", email="dude39@lp.com") in response.data assert len(outbox) == 2 assert len(recorded) == 1 gr = outbox[1] assert 'You attempted to register with a username "dude" that' in gr.body # test that signal sent. nr = recorded[0] assert not nr["existing_email"] assert not nr["user"] assert nr["existing_username"] assert nr["form_data"]["username"] == "dude" # should still get detailed errors about e.g. bad password data = dict( email="dude@lp.com", username="dude", password="a", ) response = client.post("/register", json=data) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode() == get_message( "PASSWORD_INVALID_LENGTH", length=8 ) data = dict( email="dude@lp.com", username="dd", password="awesome sunset", ) response = client.post("/register", json=data) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode() == get_message( "USERNAME_INVALID_LENGTH", min=4, max=32 ) @pytest.mark.recoverable() @pytest.mark.confirmable() @pytest.mark.settings( return_generic_responses=True, username_enable=True, send_register_email_welcome_existing_template="welcome_existing", password_confirm_required=False, ) def test_gr_extras(app, client, get_message, outbox): # If user tries to re-register - response email should contain reset password # link and (if applicable) a confirmation link data = dict( email="dude@lp.com", username="dude", password="awesome sunset", ) response = client.post("/register", json=data) assert response.status_code == 200 assert len(outbox) == 1 # now same email - same or different username data = dict( email="dude@lp.com", username="dude2", password="awesome sunset", ) response = client.post("/register", json=data) assert response.status_code == 200 assert len(outbox) == 2 matcher = re.findall(r"\w+:.*", outbox[1].body, re.IGNORECASE) kv = {m.split(":")[0]: m.split(":", 1)[1] for m in matcher} assert kv["User"] == "dude" confirm_link = kv["ConfirmationLink"] response = client.get(confirm_link) assert response.status_code in [302, 303] # now confirmed - should not get confirmation link response = client.post("/register", json=data) assert response.status_code == 200 assert len(outbox) == 3 matcher = re.findall(r"\w+:.*", outbox[2].body, re.IGNORECASE) kv = {m.split(":")[0]: m.split(":", 1)[1] for m in matcher} assert not kv["ConfirmationLink"] assert not kv["ConfirmationToken"] # but should still have reset link - test that reset_link = kv["ResetLink"] response = client.post( reset_link, json=dict(password="awesome password2", password_confirm="awesome password2"), ) assert response.status_code == 200 response = client.post( "/login", json=dict(username="dude", password="awesome password2") ) assert response.status_code == 200 @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.recoverable() @pytest.mark.confirmable() @pytest.mark.settings(return_generic_responses=True, password_confirm_required=False) def test_gr_real_html_template(app, client, get_message, outbox): # We have a test .txt template - but this will use the normal/real HTML template data = dict( email="dude@lp.com", password="awesome sunset", ) response = client.post("/register", json=data) assert response.status_code == 200 assert len(outbox) == 1 # now same email - same or different username data = dict( email="dude@lp.com", password="awesome sunset", ) response = client.post("/register", json=data) assert response.status_code == 200 assert len(outbox) == 2 matcher = re.findall( r'href="(\S*)"', outbox[1].alts["html"], re.IGNORECASE, ) assert "/reset" in matcher[0] assert "/confirm" in matcher[1] def test_subclass(app, sqlalchemy_datastore): # Test/show how to use multiple inheritance to override individual form fields. from wtforms import PasswordField, ValidationError from wtforms.validators import DataRequired from flask_security.forms import _get_form_field_label def password_validator(form, field): if field.data.startswith("PASS"): raise ValidationError("Really - don't start a password with PASS") class NewPasswordFormMixinEx: password = PasswordField( _get_form_field_label("password"), validators=[ DataRequired(message="PASSWORD_NOT_PROVIDED"), password_validator, ], ) class MyRegisterForm(NewPasswordFormMixinEx, RegisterFormV2): pass app.config["SECURITY_REGISTER_FORM"] = MyRegisterForm security = Security(datastore=sqlalchemy_datastore) security.init_app(app) client = app.test_client() data = dict( email="dude@lp.com", password="PASSmattmatt", password_confirm="PASSmattmatt", ) response = client.post( "/register", json=data, headers={"Content-Type": "application/json"} ) assert response.status_code == 400 assert "Really - don't start" in response.json["response"]["errors"][0] @pytest.mark.confirmable() @pytest.mark.settings(return_generic_responses=True) def test_my_mail_util(app, sqlalchemy_datastore): # Test that with generic_responses - we still can get syntax/validation errors. from flask_security import MailUtil, EmailValidateException class MyMailUtil(MailUtil): def validate(self, email): if email.startswith("mike"): raise EmailValidateException("No mikes allowed") init_app_with_options( app, sqlalchemy_datastore, **{"security_args": {"mail_util_cls": MyMailUtil}} ) data = dict( email="mike@lp.com", password="awesome sunset", ) client = app.test_client() response = client.post("/register", data=data) assert b"No mikes allowed" in response.data @pytest.mark.settings( register_form=RegisterFormV2, post_register_view="/post_register", ) def test_regv2(app, client, get_message): # default config should require password, password_confirm and not allow username response = client.get("/register") assert b"

    Register

    " in response.data email = get_form_input(response, "email") assert all(s in email for s in ["required", 'type="email"']) pw = get_form_input(response, "password") assert all(s in pw for s in ["required", 'type="password"']) pwc = get_form_input(response, "password_confirm") assert all(s in pwc for s in ["required", 'type="password"']) assert not get_form_input(response, "username") # check password required data = dict( email="dude@lp.com", password="", password_confirm="battery staple", next="", ) response = client.post("/register", data=data, follow_redirects=True) assert get_message("PASSWORD_NOT_PROVIDED") in response.data # check confirm required data = dict( email="dude@lp.com", password="battery staple", password_confirm="", next="", ) response = client.post("/register", data=data, follow_redirects=True) assert get_message("PASSWORD_NOT_PROVIDED") in response.data # JSON also required password_confirm in V2 response = client.post("/register", json=data) assert ( get_message("PASSWORD_NOT_PROVIDED") == response.json["response"]["errors"][0].encode() ) # check confirm matches data = dict( email="dude@lp.com", password="battery staple", password_confirm="batery staple", next="", ) response = client.post("/register", data=data, follow_redirects=True) assert get_message("RETYPE_PASSWORD_MISMATCH") in response.data data = dict( email="dude@lp.com", password="battery staple", password_confirm="battery staple", next="", ) response = client.post("/register", data=data, follow_redirects=True) assert b"Post Register" in response.data @pytest.mark.settings( register_form=RegisterFormV2, post_register_view="/post_register", password_confirm_required=False, ) def test_regv2_no_confirm(app, client, get_message): # Test password confirm not required response = client.get("/register") assert b"

    Register

    " in response.data email = get_form_input(response, "email") assert all(s in email for s in ["required", 'type="email"']) pw = get_form_input(response, "password") assert all(s in pw for s in ["required", 'type="password"']) assert not get_form_input(response, "password_confirm") assert not get_form_input(response, "username") data = dict( email="dude@lp.com", password="abc", ) response = client.post("/register", data=data, follow_redirects=True) assert get_message("PASSWORD_INVALID_LENGTH", length=8) in response.data data = dict( email="dude@lp.com", password="battery staple", password_confirm="battery terminal", # Ignored next="/my_next_idea", ) response = client.post("/register", data=data, follow_redirects=False) assert response.location == "/my_next_idea" @pytest.mark.settings( use_register_v2=True, post_register_view="/post_register", password_confirm_required=False, username_enable=True, username_required=True, ) def test_regv2_no_confirm_username(app, client, get_message): # Test password confirm not required but username is response = client.get("/register") assert b"

    Register

    " in response.data email = get_form_input(response, "email") assert all(s in email for s in ["required", 'type="email"']) pw = get_form_input(response, "password") assert all(s in pw for s in ["required", 'type="password"']) assert not get_form_input(response, "password_confirm") us = get_form_input(response, "username") assert all(s in us for s in ["required", 'type="text"']) data = dict( email="dude@lp.com", password="battery staple", ) response = client.post("/register", data=data, follow_redirects=True) assert get_message("USERNAME_NOT_PROVIDED") in response.data data = dict( email="dude@lp.com", password="battery staple", username="dude", ) response = client.post("/register", data=data, follow_redirects=True) assert b"Post Register" in response.data @pytest.mark.settings( use_register_v2=True, password_confirm_required=False, ) def test_subclass_v2(app, sqlalchemy_datastore): # Test that is create our own RegisterForm the USE_REGISTER_V2 config is ignored class MyRegisterForm(RegisterFormV2): from wtforms.validators import Length myfield = StringField(label="My field", validators=[Length(min=8)]) app.config["SECURITY_REGISTER_FORM"] = MyRegisterForm security = Security(datastore=sqlalchemy_datastore) security.init_app(app) client = app.test_client() data = dict( email="dude@lp.com", password="battery staple", myfield="short", ) response = client.post("/register", json=data) assert ( response.json["response"]["errors"][0] == "Field must be at least 8 characters long." ) pallets-eco-flask-security-72bf929/tests/test_response.py000066400000000000000000000120751525721743500236640ustar00rootroot00000000000000""" test_response ~~~~~~~~~~~~~~~~~ Tests for validating default and plugable responses. :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import pytest from flask import jsonify from tests.test_utils import authenticate def test_render_json(app, client): @app.security.render_json def my_json(payload, code, headers=None, user=None): return jsonify(dict(myresponse=payload, code=code)) response = client.get( "/login", data={}, headers={"Content-Type": "application/json"} ) assert "myresponse" in response.json assert response.json["code"] == 200 def _my_json(payload, code, headers=None, user=None): return jsonify(dict(myresponse=payload, code=code)) def test_render_json2(app, client): app.extensions["security"].render_json(_my_json) response = client.get( "/login", data={}, headers={"Content-Type": "application/json"} ) assert "myresponse" in response.json assert response.json["code"] == 200 def test_render_json_logout(app, client): app.extensions["security"].render_json(_my_json) response = client.post("/logout", headers={"Content-Type": "application/json"}) assert "myresponse" in response.json assert response.json["code"] == 200 def test_default_unauthn(app, client): """Test default unauthn handler with and without json""" response = client.get("/profile") assert response.status_code in [302, 303] assert response.location == "/login?next=/profile" response = client.get("/profile", headers={"Accept": "application/json"}) assert response.status_code == 401 assert response.json["meta"]["code"] == 401 # While "Basic" is acceptable, we never get a WWW-Authenticate header back since # most browsers intercept it. assert "WWW-Authenticate" not in response.headers @pytest.mark.settings(login_url="/mylogin", url_prefix="/myprefix") def test_default_unauthn_bp(app, client): """Test default unauthn handler with blueprint prefix and login url""" response = client.get("/profile") assert response.status_code in [302, 303] assert response.location == "/myprefix/mylogin?next=/profile" def test_default_unauthn_myjson(app, client): """Make sure render_json gets called for unauthn errors""" @app.security.render_json def my_json(payload, code, headers=None, user=None): return jsonify(dict(myresponse=payload, code=code)), code, headers response = client.get("/multi_auth", headers={"Accept": "application/json"}) assert response.status_code == 401 assert response.json["code"] == 401 assert "myresponse" in response.json def test_my_unauthn_handler(app, client): @app.security.unauthn_handler def my_unauthn(mechanisms, headers=None): return app.security._render_json({"mechanisms": mechanisms}, 401, headers, None) response = client.get("/multi_auth", headers={"Accept": "application/json"}) assert response.status_code == 401 assert all( m in response.json["response"]["mechanisms"] for m in ["session", "token", "basic"] ) def test_default_unauthz(app, client): """Test default unauthz handler with and without json""" authenticate(client, "joe@lp.com", "password") response = client.get("/admin") # This is the result of abort(403) since there is no UNAUTHORIZED_VIEW assert response.status_code == 403 response = client.get("/admin", headers={"Accept": "application/json"}) assert response.status_code == 403 assert response.json["meta"]["code"] == 403 def test_default_unauthz_myjson(app, client): """Make sure render_json gets called for unauthn errors""" @app.security.render_json def my_json(payload, code, headers=None, user=None): return jsonify(dict(myresponse=payload, code=code)), code, headers authenticate(client, "joe@lp.com", "password") response = client.get("/admin", headers={"Accept": "application/json"}) assert response.status_code == 403 assert response.json["code"] == 403 def test_my_unauthz_handler(app, client): @app.security.unauthz_handler def my_unauthz(func_name, params): return ( jsonify(dict(myresponse={"func": func_name, "params": params}, code=403)), 403, ) authenticate(client, "joe@lp.com", "password") response = client.get("/admin", headers={"Accept": "application/json"}) assert response.status_code == 403 assert response.json["code"] == 403 assert response.json["myresponse"]["func"] == "roles_required" assert response.json["myresponse"]["params"] == ["admin"] def test_my_unauthz_handler_exc(app, client): """Verify that can use exceptions in unauthz handler""" @app.security.unauthz_handler def my_unauthz(func, params): raise ValueError("Bad Value") @app.errorhandler(ValueError) def error_handler(ex): return jsonify(dict(code=403)), 403 authenticate(client, "joe@lp.com", "password") response = client.get("/admin", headers={"Accept": "application/json"}) assert response.status_code == 403 pallets-eco-flask-security-72bf929/tests/test_templates.py000066400000000000000000000170241525721743500240230ustar00rootroot00000000000000""" test_templates ~~~~~~~~~~ Test templates to be W3C valid :copyright: (c) 2025-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. Note that the validator definitely rate-limits us - so we don't just run them all. To run a test pass templates="RESET,LOGIN" on the command line. """ import re from time import sleep import pytest import requests from flask import g from tests.test_utils import ( authenticate, logout, capture_reset_password_requests, get_form_input_value, reset_fresh, setup_tf_sms, ) from tests.test_webauthn import HackWebauthnUtil, reg_first_key def check_message(msgs, mtype="error"): # list of JSON messages from validator errors = [] for msg in msgs: if msg["type"] == mtype: errors.append(msg["message"]) return errors def check_template(name, client, r): # returns list of validation errors t = client.get(name) if t.status_code != 200: return [f"{name} error {t.status_code}"] return check_template_rdata(name, r, t.data) def check_template_rdata(name, r, rdata): vout = r.post("https://validator.w3.org/nu/?out=json", rdata) if vout.status_code == 429: # we got rate limited try again sleep(2.0) vout = r.post("https://validator.w3.org/nu/?out=json", rdata) if vout.status_code != 200: return [f"{name} API error {vout.status_code}"] if vout.status_code != 200: return [f"{name} API error {vout.status_code}"] return check_message(vout.json()["messages"]) def get_script_tags(html): """Return list of script tags in the HTML""" return re.findall(r"]*>", html, re.DOTALL | re.IGNORECASE) @pytest.mark.registerable() @pytest.mark.recoverable() @pytest.mark.changeable() @pytest.mark.change_email() @pytest.mark.change_username() @pytest.mark.oauth() @pytest.mark.username_recovery() @pytest.mark.unified_signin() @pytest.mark.webauthn(webauthn_util_cls=HackWebauthnUtil) @pytest.mark.two_factor() @pytest.mark.settings( multi_factor_recovery_codes=True, oauth_enable=True, ) @pytest.mark.csrf() def test_valid_html(app, client): # since we get rate limited - use external pytest option to specify totry = app.config.get("TEMPLATES", "").split(",") rsession = requests.session() rsession.headers.update({"Content-Type": "text/html; charset=utf-8"}) unauth_urls = [ "LOGIN", "REGISTER", "RESET", "US_SIGNIN", "USERNAME_RECOVERY", "WAN_SIGNIN", ] auth_urls = [ "CHANGE", "CHANGE_USERNAME", "CHANGE_EMAIL", "MULTI_FACTOR_RECOVERY_CODES", "TWO_FACTOR_SETUP", "US_SETUP", "US_VERIFY", "WAN_REGISTER", "WAN_VERIFY", ] # MULTI_FACTOR_RECOVERY requires tf-setup and login/password # TWO_FACTOR_RESCUE has an issue with the RadioField # TWO_FACTOR_SELECT needs special setup authenticate(client, csrf=True) response = client.get("/change") csrf_token = get_form_input_value(response, "csrf_token") reg_first_key(client, csrf_token=csrf_token) logout(client) terrors = dict() for t in [u for u in unauth_urls if u in totry]: terrors[t] = check_template(app.config[f"SECURITY_{t}_URL"], client, rsession) authenticate(client, csrf=True) for t in [u for u in auth_urls if u in totry]: if t == "US_SETUP": response = client.get("/us-setup") csrf_token = get_form_input_value(response, "csrf_token") response = client.post( "us-setup", data=dict(chosen_method="authenticator", csrf_token=csrf_token), ) terrors[t] = check_template_rdata("US_SETUP", rsession, response.data) continue if t == "TWO_FACTOR_SETUP": response = client.get("/tf-setup") csrf_token = get_form_input_value(response, "csrf_token") response = client.post( "tf-setup", data=dict(setup="authenticator", csrf_token=csrf_token), ) terrors[t] = check_template_rdata( "TWO_FACTOR_SETUP", rsession, response.data ) continue elif t == "US_VERIFY" or t == "VERIFY": reset_fresh(client, app.config["SECURITY_FRESHNESS"]) terrors[t] = check_template(app.config[f"SECURITY_{t}_URL"], client, rsession) print(f"Validated: {totry}") errors = {k: v for k, v in terrors.items() if v} assert not any(errors), errors @pytest.mark.confirmable() @pytest.mark.csrf() def test_valid_html_confirm(app, client): rsession = requests.session() rsession.headers.update({"Content-Type": "text/html; charset=utf-8"}) # since we get rate limited - use external pytest option to specify totry = app.config.get("TEMPLATES", "").split(",") if "CONFIRM" in totry: print(f"Validated: {totry}") terrors = check_template(app.config["SECURITY_CONFIRM_URL"], client, rsession) assert not terrors @pytest.mark.recoverable() @pytest.mark.csrf() def test_valid_html_recover(app, client): rsession = requests.session() rsession.headers.update({"Content-Type": "text/html; charset=utf-8"}) # since we get rate limited - use external pytest option to specify totry = app.config.get("TEMPLATES", "").split(",") if "RESET" in totry: print(f"Validated: {totry}") with capture_reset_password_requests() as resets: response = client.get("/reset") csrf_token = get_form_input_value(response, "csrf_token") client.post("/reset", data=dict(email="joe@lp.com", csrf_token=csrf_token)) token = resets[0]["token"] terrors = check_template( f'{app.config[f"SECURITY_RESET_URL"]}/{token}', client, rsession ) assert not terrors @pytest.mark.two_factor() @pytest.mark.csrf() def test_valid_html_rescue(app, client): rsession = requests.session() rsession.headers.update({"Content-Type": "text/html; charset=utf-8"}) # since we get rate limited - use external pytest option to specify totry = app.config.get("TEMPLATES", "").split(",") if "TWO_FACTOR_RESCUE" in totry: authenticate(client, csrf=True) response = client.get("/tf-setup") csrf_token = get_form_input_value(response, "csrf_token") setup_tf_sms(client, csrf_token=csrf_token) logout(client) authenticate(client, csrf=True) print(f"Validated: {totry}") terrors = check_template( app.config["SECURITY_TWO_FACTOR_RESCUE_URL"], client, rsession ) assert not terrors @pytest.mark.webauthn(webauthn_util_cls=HackWebauthnUtil) def test_script_nonce(app, client): """Test that script nonces appear in script tags when configured.""" @app.before_request def set_nonce(): g.csp_nonce = "12345" # default config -> no nonces in script tags res = client.get("/wan-signin") tags = get_script_tags(res.text) assert tags assert all("nonce" not in t for t in tags) # invalid nonce key given -> no nonces in script tags app.config["SECURITY_SCRIPT_NONCE_KEY"] = "wrong_key" res = client.get("/wan-signin") tags = get_script_tags(res.text) assert tags assert all("nonce" not in t for t in tags) # nonces set up correctly -> nonce in script tags app.config["SECURITY_SCRIPT_NONCE_KEY"] = "csp_nonce" res = client.get("/wan-signin") tags = get_script_tags(res.text) assert tags assert all('nonce="12345"' in t for t in tags) pallets-eco-flask-security-72bf929/tests/test_tf_plugin.py000066400000000000000000000106571525721743500240210ustar00rootroot00000000000000""" test_tf_plugin ~~~~~~~~~~~~~~~~~ tf_plugin tests :copyright: (c) 2022-2025 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import json import pytest from tests.test_utils import ( get_form_action, get_session, get_existing_session, logout, setup_tf_sms, ) from tests.test_two_factor import tf_in_session from tests.test_webauthn import HackWebauthnUtil, wan_signin, reg_2_keys pytest.importorskip("webauthn") @pytest.mark.webauthn(webauthn_util_cls=HackWebauthnUtil) @pytest.mark.two_factor() def test_tf_select(app, client, get_message): # Test basic select mechanism when more than one 2FA has been setup wankeys = reg_2_keys(client) # add a webauthn 2FA key (authenticates) sms_sender = setup_tf_sms(client) logout(client) # since we have 2 2FA methods configured - we should get the tf-select form # also - test that we correctly propagate 'next' all the way through response = client.post( "/login?next=/profile", data=dict(email="matt@lp.com", password="password"), follow_redirects=True, ) assert b"Select Two-Factor Method" in response.data tf_select_url = get_form_action(response) response = client.post( tf_select_url, data=dict(which="webauthn"), follow_redirects=True ) assert b"Use a Passkey as a Second Factor" in response.data wan_signin_url = get_form_action(response) assert "/wan-signin?next=/profile" == wan_signin_url response = wan_signin( client, "matt@lp.com", wankeys["secondary"]["signin"], wan_signin_url ) assert not tf_in_session(get_existing_session(client)) assert b"Profile Page" in response.data # now do other 2FA logout(client) response = client.post( "/login", data=dict(email="matt@lp.com", password="password"), follow_redirects=True, ) assert b"Select Two-Factor Method" in response.data response = client.post("/tf-select", data=dict(which="sms"), follow_redirects=True) assert b"Please enter your authentication code generated via: SMS" in response.data code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"Your code has been confirmed" in response.data assert not tf_in_session(get_session(response)) # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 assert not tf_in_session(get_existing_session(client)) @pytest.mark.webauthn(webauthn_util_cls=HackWebauthnUtil) @pytest.mark.two_factor() def test_tf_select_json(app, client, get_message): # Test basic select mechanism when more than one 2FA has been setup headers = {"Accept": "application/json", "Content-Type": "application/json"} wankeys = reg_2_keys(client) # add a webauthn 2FA key (authenticates) setup_tf_sms(client) logout(client) # since we have 2 2FA methods configured - we should get the tf-select form response = client.post( "/login", json=dict(email="matt@lp.com", password="password") ) assert response.json["response"]["tf_required"] choices = response.json["response"]["tf_setup_methods"] assert all(k in choices for k in ["sms", "webauthn"]) # should get same answer for GET on /tf-select response = client.get("/tf-select", headers=headers) choices = response.json["response"]["tf_setup_methods"] assert all(k in choices for k in ["sms", "webauthn"]) # use webauthn as the second factor response = client.post("/tf-select", json=dict(which="webauthn")) signin_url = response.json["response"]["tf_signin_url"] response = client.post(signin_url, headers=headers) response_url = f'wan-signin/{response.json["response"]["wan_state"]}' response = client.post( response_url, json=dict(credential=json.dumps(wankeys["secondary"]["signin"])), ) assert response.status_code == 200 assert not tf_in_session(get_existing_session(client)) response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.two_factor() @pytest.mark.settings(url_prefix="/api") def test_tf_select_auth(app, client, get_message): # /tf-select is an unauthenticated endpoint - make sure only allowable in correct # state. response = client.get("/api/tf-select", follow_redirects=False) assert "/api/login" in response.location pallets-eco-flask-security-72bf929/tests/test_tokens.py000066400000000000000000000424601525721743500233320ustar00rootroot00000000000000""" test_tokens ~~~~~~~~~~~~~~~~ Refresh/Auth Token functionality tests :copyright: (c) 2026-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from datetime import datetime, timedelta, timezone import math import pytest from freezegun import freeze_time from itsdangerous import SignatureExpired from sqlalchemy import Column, String from flask_security.datastore import PeeweeDatastore from flask_security.tokens import RefreshTokenErrors from flask_security.utils import parse_auth_token from tests.test_csrf import _get_csrf_token from tests.test_utils import ( check_signals, json_authenticate, logout, FakeSerializer, verify_token, ) pytestmark = pytest.mark.refresh_token() def _set_refresh_tracker(app, refresh_token, **kwargs): with app.app_context(): tdata = app.security.refresh_token_serializer.loads(refresh_token) refresh_tracker = app.security.datastore.find_refresh_tracker(tdata["family"]) for k, v in kwargs.items(): setattr(refresh_tracker, k, v) app.security.datastore.put(refresh_tracker) app.security.datastore.commit() if isinstance(app.security.datastore, PeeweeDatastore): app.security.datastore.db.close_db(None) @pytest.mark.app_settings(testing_no_cookies=True) @pytest.mark.settings(refresh_token_cookie_name=None) def test_refresh(app, clients, get_message): """Test basic refresh token flow. - authenticate and receive auth_token and refresh_token - we hack the auth_token to be already expired - we should get a 401 when using it - use the /refresh-token endpoint to get a new refresh_token and auth_token - auth_token should be valid """ start_ts = datetime.now(timezone.utc).replace(tzinfo=None).timestamp() max_age = app.config["SECURITY_REFRESH_TOKEN_MAX_AGE"] rts = app.security.remember_token_serializer app.security.remember_token_serializer = FakeSerializer(2.0) response = json_authenticate(clients) with pytest.raises(TypeError): clients.get_cookie("session") auth_token1 = response.json["response"]["user"]["authentication_token"] refresh_token = response.json["response"]["user"]["refresh_token"] tdict = app.security.refresh_token_serializer.loads(refresh_token) assert ( tdict["expires_at"] > ((datetime.today() + max_age) - timedelta(days=1)).timestamp() ) assert tdict["gen"] == 1 assert tdict["last_used_at"] >= start_ts verify_token(clients, auth_token1, 401) # set auth_token serializer back to one that doesn't expire app.security.remember_token_serializer = rts response = clients.post("/refresh-token", json={"refresh_token": refresh_token}) new_refresh_token = response.json["response"]["user"]["refresh_token"] auth_token = response.json["response"]["user"]["authentication_token"] verify_token(clients, auth_token) ntdict = app.security.refresh_token_serializer.loads(new_refresh_token) assert ( ntdict["expires_at"] > ((datetime.today() + max_age) - timedelta(days=1)).timestamp() ) assert ntdict["gen"] == 2 assert tdict["family"] == ntdict["family"] # mongo stores more digits... te = math.floor(tdict["expires_at"] * 1000.0) nte = math.floor(ntdict["expires_at"] * 1000.0) assert te == nte @pytest.mark.app_settings(testing_no_cookies=True) @pytest.mark.settings(refresh_token_cookie_name=None) def test_refresh_errors(app, client, get_message): response = json_authenticate(client) refresh_token = response.json["response"]["user"]["refresh_token"] response = client.post("/refresh-token", json={"refresh_token": ""}) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "This field is required." response = client.post("/refresh-token", json={"refresh_token": "12345"}) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "REFRESH_TOKEN_INVALID", reason=RefreshTokenErrors.INVALID.name ) # This is a JSON endpoint only. response = client.post("/refresh-token", data={"refresh_token": refresh_token}) assert response.status_code == 400 # now delete the tracker from the server response = client.post("/refresh-token", json={"refresh_token": refresh_token}) assert response.status_code == 200 with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") assert len(user.refresh_trackers) == 1 app.security.datastore.delete(user.refresh_trackers[0]) app.security.datastore.commit() response = client.post("/refresh-token", json={"refresh_token": refresh_token}) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "REFRESH_TOKEN_INVALID", reason=RefreshTokenErrors.NOT_FOUND.name ) @pytest.mark.app_settings(testing_no_cookies=True) @pytest.mark.settings( refresh_token_max_idle=timedelta(hours=1), refresh_token_cookie_name=None ) def test_idle_expire(app, clients, get_message): response = json_authenticate(clients) refresh_token = response.json["response"]["user"]["refresh_token"] new_last_used_at = app.security.datetime_factory() - timedelta(days=1) _set_refresh_tracker(app, refresh_token, last_used_at=new_last_used_at) response = clients.post("/refresh-token", json={"refresh_token": refresh_token}) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "REFRESH_TOKEN_INVALID", reason=RefreshTokenErrors.EXPIRED.name ) @pytest.mark.settings(refresh_token_cookie_name=None) def test_gen_mismatch(app, clients, get_message, signals): # If there is generation mismatch, the system should revoke the # entire refresh token family. response = json_authenticate(clients) refresh_token1 = response.json["response"]["user"]["refresh_token"] response = clients.post("/refresh-token", json={"refresh_token": refresh_token1}) refresh_token2 = response.json["response"]["user"]["refresh_token"] auth_token2 = response.json["response"]["user"]["authentication_token"] verify_token(clients, auth_token2) # now use refresh_token1 again and the system should revoke the entire # refresh token family and a signal sent response = clients.post("/refresh-token", json={"refresh_token": refresh_token1}) assert response.status_code == 400 rsignal = signals["refresh_tracker_revoked"][0] assert rsignal["request_endpoint"] == "security.refresh_token" assert rsignal["refresh_errors"] == RefreshTokenErrors.GEN_MISMATCH # now use the supposedly good refresh_token - it too should have been revoked response = clients.post("/refresh-token", json={"refresh_token": refresh_token2}) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "REFRESH_TOKEN_INVALID", reason=RefreshTokenErrors.REVOKED.name ) check_signals(signals, ["refresh_tracker_revoked", "refresh_tracker_created"]) @pytest.mark.app_settings(testing_no_cookies=True) @pytest.mark.settings(refresh_token_cookie_name=None) def test_auto_cleanup_expired(app, clients, get_message, signals): # test that expired refresh trackers are cleaned up as part of getting a new tracker response = json_authenticate(clients) refresh_token = response.json["response"]["user"]["refresh_token"] new_expires_at = app.security.datetime_factory() - timedelta(days=1) _set_refresh_tracker(app, refresh_token, expires_at=new_expires_at) response = clients.post("/refresh-token", json={"refresh_token": refresh_token}) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "REFRESH_TOKEN_INVALID", reason=RefreshTokenErrors.EXPIRED.name ) # log out and back in and should get a new refresh token logout(clients) response = json_authenticate(clients) check_signals(signals, {"refresh_tracker_created": 2}) with app.app_context(): user = app.security.datastore.find_user(email="matt@lp.com") assert len(user.refresh_trackers) == 1 @pytest.mark.app_settings(testing_no_cookies=True) @pytest.mark.settings(refresh_token_cookie_name=None) def test_reset(app, clients, get_message): response = json_authenticate(clients) refresh_token = response.json["response"]["user"]["refresh_token"] with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") assert len(user.refresh_trackers) == 1 app.security.datastore.refresh_tracker_reset(user) app.security.datastore.commit() response = clients.post("/refresh-token", json={"refresh_token": refresh_token}) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "REFRESH_TOKEN_INVALID", reason=RefreshTokenErrors.REVOKED.name ) class UserWithTokenUniquifier: fs_token_uniquifier = Column(String(64), unique=True, nullable=False) @pytest.mark.app_settings( TESTING_USER_MIXIN=UserWithTokenUniquifier, testing_no_cookies=True ) @pytest.mark.settings(refresh_token_cookie_name=None) @pytest.mark.changeable() def test_token_uniquifier(app, client, get_message): # Test if User model has a fs_token_uniquifier field then change password doesn't # affect refresh tokens. with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") assert user.fs_token_uniquifier is not None response = json_authenticate(client) refresh_token = response.json["response"]["user"]["refresh_token"] auth_token = response.json["response"]["user"]["authentication_token"] headers = {"Authentication-Token": auth_token} response = client.post( "/change", json=dict( password="password", new_password="awesome sunset", new_password_confirm="awesome sunset", ), headers=headers, ) assert response.status_code == 200 response = client.post("/refresh-token", json={"refresh_token": refresh_token}) assert response.status_code == 200 @pytest.mark.app_settings(testing_no_cookies=True) @pytest.mark.changeable() @pytest.mark.settings(refresh_token_cookie_name=None) def test_no_token_uniquifier(app, client, get_message): # Test if User model doesn't have a fs_token_uniquifier field then change # password will invalidate all refresh tokens. response = json_authenticate(client) refresh_token = response.json["response"]["user"]["refresh_token"] auth_token = response.json["response"]["user"]["authentication_token"] headers = {"Authentication-Token": auth_token} response = client.post( "/change", json=dict( password="password", new_password="awesome sunset", new_password_confirm="awesome sunset", ), headers=headers, ) assert response.status_code == 200 response = client.post("/refresh-token", json={"refresh_token": refresh_token}) assert response.status_code == 400 @pytest.mark.app_settings(testing_no_cookies=True) @pytest.mark.settings(refresh_token_cookie_name=None) def test_logout_with_token(app, client, get_message): response = json_authenticate(client) refresh_token = response.json["response"]["user"]["refresh_token"] auth_token = response.json["response"]["user"]["authentication_token"] headers = {"Authentication-Token": auth_token} client.post("/logout", json=dict(refresh_token=refresh_token), headers=headers) response = client.post("/refresh-token", json={"refresh_token": refresh_token}) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "REFRESH_TOKEN_INVALID", reason=RefreshTokenErrors.REVOKED.name ) with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") assert len(user.refresh_trackers) == 1 assert user.refresh_trackers[0].revoked_at is not None @pytest.mark.settings( refresh_token_cookie_name="fs_rtoken_test", refresh_token_cookie=dict( samesite="Lax", httponly=True, secure=True, ), ) def test_refresh_cookie(app, client, get_message): """If we configure a cookie we shouldn't get the refresh token in the response. The cookie should be properly updated when getting a new auth_token. The cookie should be deleted as part of logout. """ response = json_authenticate(client) assert "refresh_token" not in response.json["response"]["user"] rcookie = client.get_cookie("fs_rtoken_test") assert rcookie is not None assert rcookie.http_only assert rcookie.same_site == "Lax" assert rcookie.secure tdict = app.security.refresh_token_serializer.loads(rcookie.value) assert tdict["gen"] == 1 auth_token = response.json["response"]["user"]["authentication_token"] verify_token(client, auth_token) headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.post("/refresh-token", headers=headers) assert response.status_code == 200 nrcookie = client.get_cookie("fs_rtoken_test") assert nrcookie is not None assert nrcookie.http_only ntdict = app.security.refresh_token_serializer.loads(nrcookie.value) assert ntdict["gen"] == 2 assert tdict["family"] == ntdict["family"] # log out and ensure cookie is removed client.post("/logout") assert client.get_cookie("fs_rtoken_test") is None assert len(client._cookies) == 1 # session with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") assert len(user.refresh_trackers) == 1 assert user.refresh_trackers[0].revoked_at is not None @pytest.mark.csrf(csrfprotect=True) def test_refresh_token_csrf(app, client, get_message): csrf_token = _get_csrf_token(client) headers = {"X-CSRF-Token": csrf_token} json_authenticate(client, headers=headers) rcookie = client.get_cookie("fs_refresh") assert rcookie is not None response = client.post("/refresh-token", json={}) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." client.post("/refresh-token", json={}, headers=headers) nrcookie = client.get_cookie("fs_refresh") assert nrcookie is not None ntdict = app.security.refresh_token_serializer.loads(nrcookie.value) assert ntdict["gen"] == 2 # upon logout refresh tracker/token should have been revoked client.post("/logout") assert client.get_cookie("fs_rtoken_test") is None with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") assert len(user.refresh_trackers) == 1 assert user.refresh_trackers[0].revoked_at is not None @pytest.mark.settings(token_max_age=60) def test_auth_token_max_age_int(app, client_nc): with app.app_context(): with freeze_time(datetime.now(timezone.utc) + timedelta(minutes=-3)): response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] with pytest.raises(SignatureExpired): parse_auth_token(token) @pytest.mark.settings(token_max_age=timedelta(days=3)) def test_auth_token_max_age_delta(app, client_nc): with app.app_context(): with freeze_time(datetime.now(timezone.utc) + timedelta(days=-5)): response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] with pytest.raises(SignatureExpired): parse_auth_token(token) @pytest.mark.settings(token_max_age=0) def test_auth_token_max_age_0(app, client_nc): with app.app_context(): with freeze_time(datetime.now(timezone.utc) + timedelta(weeks=-1000)): response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] with pytest.raises(SignatureExpired): parse_auth_token(token) @pytest.mark.settings(token_max_age=timedelta(days=6)) def test_per_user_expired_token(app, client_nc): # Test expiry in auth_token using callable with app.app_context(): with freeze_time(datetime.now(timezone.utc) + timedelta(days=-3)): def exp(user): assert user.email == "matt@lp.com" return int((datetime.now(timezone.utc) + timedelta(days=1)).timestamp()) app.config["SECURITY_TOKEN_EXPIRE_TIMESTAMP"] = exp response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] with pytest.raises(SignatureExpired) as e: parse_auth_token(token) assert "token[exp] value expired" in e.value.message def test_per_user_not_expired_token(app, client_nc): # Test expiry in auth_token using callable def exp(user): assert user.email == "matt@lp.com" return int((datetime.now(timezone.utc) + timedelta(days=1)).timestamp()) app.config["SECURITY_TOKEN_EXPIRE_TIMESTAMP"] = exp response = json_authenticate(client_nc) token = response.json["response"]["user"]["authentication_token"] verify_token(client_nc, token) pallets-eco-flask-security-72bf929/tests/test_trackable.py000066400000000000000000000063051525721743500237550ustar00rootroot00000000000000""" test_trackable ~~~~~~~~~~~~~~ Trackable tests """ import datetime import pytest from flask import after_this_request, redirect from werkzeug.middleware.proxy_fix import ProxyFix from flask_security import login_user from tests.test_utils import authenticate, logout pytestmark = pytest.mark.trackable() def _client_ip(client): """Compatibility layer for Flask<0.12.""" return getattr(client, "environ_base", {}).get("REMOTE_ADDR") def test_trackable_flag(app, client): app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1) e = "matt@lp.com" authenticate(client, email=e) logout(client) authenticate(client, email=e, headers={"X-Forwarded-For": "127.0.0.1"}) with app.app_context(): user = app.security.datastore.find_user(email=e) assert user.last_login_at is not None assert user.current_login_at is not None assert user.last_login_ip == _client_ip(client) assert user.current_login_ip == "127.0.0.1" assert user.login_count == 2 def test_trackable_with_multiple_ips_in_headers(app, client): app.wsgi_app = ProxyFix(app.wsgi_app, x_for=2) e = "matt@lp.com" authenticate(client, email=e) logout(client) authenticate( client, email=e, headers={"X-Forwarded-For": "99.99.99.99, 88.88.88.88, 77.77.77.77"}, ) with app.app_context(): user = app.security.datastore.find_user(email=e) assert user.last_login_at is not None assert user.current_login_at is not None assert user.last_login_ip == _client_ip(client) assert user.current_login_ip == "88.88.88.88" assert user.login_count == 2 def test_trackable_using_login_user(app, client): """ This tests is only to serve as an example of how one needs to call datastore.commit() after logging a user in to make sure the trackable fields are saved to the datastore. """ app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1) @app.route("/login_custom", methods=["POST"]) def login_custom(): user = app.security.datastore.find_user(email=e) login_user(user) @after_this_request def save_user(response): app.security.datastore.commit() return response return redirect("/") e = "matt@lp.com" authenticate(client, email=e) logout(client) data = dict(email=e, password="password", remember="y") client.post("/login_custom", data=data, headers={"X-Forwarded-For": "127.0.0.1"}) with app.app_context(): user = app.security.datastore.find_user(email=e) assert user.last_login_at is not None assert user.current_login_at is not None assert user.last_login_ip == _client_ip(client) assert user.current_login_ip == "127.0.0.1" assert user.login_count == 2 @pytest.mark.settings(datetime_factory=lambda: datetime.datetime(2024, 3, 24)) def test_trackable_datetime(app, client): app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1) e = "matt@lp.com" authenticate(client, email=e) with app.app_context(): user = app.security.datastore.find_user(email=e) assert user.current_login_at == datetime.datetime(2024, 3, 24) assert user.login_count == 1 pallets-eco-flask-security-72bf929/tests/test_two_factor.py000066400000000000000000002042471525721743500242010ustar00rootroot00000000000000""" test_two_factor ~~~~~~~~~~~~~~~~~ two_factor tests :copyright: (c) 2019-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from datetime import date, timedelta import re from freezegun import freeze_time import markupsafe from passlib.totp import TOTP import pytest from flask_principal import identity_changed from flask_security import ( SQLAlchemyUserDatastore, SmsSenderFactory, reset_password_instructions_sent, tf_profile_changed, uia_email_mapper, ) from flask_security.utils import localize_callback from tests.test_utils import ( SmsBadSender, SmsTestSender, authenticate, capture_flashes, capture_send_code_requests, check_location, get_form_action, get_form_input_value, get_session, is_authenticated, json_authenticate, logout, check_signals, ) pytestmark = pytest.mark.two_factor() SmsSenderFactory.senders["test"] = SmsTestSender SmsSenderFactory.senders["bad"] = SmsBadSender def tf_authenticate(app, client, json=False, validate=True, remember=False): """Login/Authenticate using two factor. This is the equivalent of utils:authenticate """ prev_sms = app.config["SECURITY_SMS_SERVICE"] app.config["SECURITY_SMS_SERVICE"] = "test" sms_sender = SmsSenderFactory.createSender("test") json_data = dict(email="gal@lp.com", password="password", remember=remember) response = client.post( "/login", json=json_data, headers={"Content-Type": "application/json"} ) assert b'"code": 200' in response.data app.config["SECURITY_SMS_SERVICE"] = prev_sms if validate: code = sms_sender.messages[0].split()[-1] if json: response = client.post( "/tf-validate", json=dict(code=code), ) assert response.status_code == 200 else: response = client.post( "/tf-validate", data=dict(code=code), follow_redirects=True ) assert response.status_code == 200 def tf_in_session(session): return any( k in session for k in [ "tf_state", "tf_primary_method", "tf_user_id", "tf_remember_login", "tf_totp_secret", "tf_select", ] ) @pytest.mark.settings( two_factor_always_validate=False, two_factor_validity_cookie_name="tfv" ) def test_validity_cookie(app, client, get_message): tf_authenticate(app, client, remember=True) assert client.get_cookie("tfv") # logout shouldn't delete cookie so we can log in again with just password logout(client) data = dict(email="gal@lp.com", password="password") response = client.post("/login", data=data, follow_redirects=True) assert b"Welcome gal@lp.com" in response.data assert response.status_code == 200 logout(client) # ensure the cookie is for a specific user data = dict(email="gal2@lp.com", password="password") response = client.post("/login", data=data, follow_redirects=True) assert b"Please enter your authentication code" in response.data client.delete_cookie("tfv") # Same tests as above with JSON (this authenticates gal@lp.com) tf_authenticate(app, client, json=True, remember=True) assert client.get_cookie("tfv") logout(client) # shouldn't require tf data = dict(email="gal@lp.com", password="password") response = client.post( "/login", json=data, follow_redirects=True, ) assert response.status_code == 200 is_authenticated(client, get_message) logout(client) data["email"] = "gal2@lp.com" response = client.post( "/login", json=data, follow_redirects=True, ) assert response.status_code == 200 assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "ready" assert response.json["response"]["tf_primary_method"] == "authenticator" @pytest.mark.settings(two_factor_always_validate=False) def test_do_not_remember_tf_validity(app, client): # If 'remember' not set during login - no cookie should be sent # and tf should be required every time. tf_authenticate(app, client) assert not client.get_cookie("tf_validity") logout(client) data = dict(email="gal@lp.com", password="password") response = client.post("/login", data=data, follow_redirects=True) assert b"Please enter your authentication code" in response.data # Test JSON tf_authenticate(app, client, json=True) logout(client) assert not client.get_cookie("tf_validity") data = dict(email="gal@lp.com", password="password") response = client.post( "/login", json=data, follow_redirects=True, headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "ready" assert response.json["response"]["tf_primary_method"] == "sms" @pytest.mark.settings( two_factor_always_validate=False, two_factor_login_validity=timedelta(minutes=-1) ) def test_tf_expired_cookie(app, client): tf_authenticate(app, client, remember=True) logout(client) data = dict(email="gal@lp.com", password="password") response = client.post("/login", data=data, follow_redirects=True) assert b"Please enter your authentication code" in response.data # Test JSON tf_authenticate(app, client, json=True, remember=True) logout(client) data = dict(email="gal@lp.com", password="password") response = client.post( "/login", json=data, follow_redirects=True, headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "ready" assert response.json["response"]["tf_primary_method"] == "sms" @pytest.mark.settings(two_factor_always_validate=False) def test_change_uniquifier_invalidates_cookie(app, client): tf_authenticate(app, client, remember=True) logout(client) with app.app_context(): user = app.security.datastore.find_user(email="gal@lp.com") app.security.datastore.set_uniquifier(user) app.security.datastore.commit() data = dict(email="gal@lp.com", password="password") response = client.post("/login", data=data, follow_redirects=True) assert b"Please enter your authentication code" in response.data client.delete_cookie("tf_validity") # Test JSON tf_authenticate(app, client, json=True, remember=True) logout(client) with app.app_context(): user = app.security.datastore.find_user(email="gal@lp.com") app.security.datastore.set_uniquifier(user) app.security.datastore.commit() data = dict(email="gal@lp.com", password="password") response = client.post( "/login", json=data, follow_redirects=True, headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "ready" assert response.json["response"]["tf_primary_method"] == "sms" @pytest.mark.settings(two_factor_always_validate=False, two_factor_required=True) def test_tf_reset_invalidates_cookie(app, client): tf_authenticate(app, client, remember=True) logout(client) with app.app_context(): user = app.security.datastore.find_user(email="gal@lp.com") app.security.datastore.reset_user_access(user) app.security.datastore.commit() data = dict(email="gal@lp.com", password="password") response = client.post("/login", data=data, follow_redirects=True) assert b"Two-Factor authentication adds an extra layer of security" in response.data client.delete_cookie("tf_validity") # Test JSON tf_authenticate(app, client, json=True, remember=True, validate=False) logout(client) with app.app_context(): user = app.security.datastore.find_user(email="gal@lp.com") app.security.datastore.reset_user_access(user) app.security.datastore.commit() data = dict(email="gal@lp.com", password="password") response = client.post( "/login", json=data, follow_redirects=True, headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "setup_from_login" @pytest.mark.settings(two_factor_required=True) def test_two_factor_two_factor_setup_anonymous(app, client, get_message): # trying to pick method without doing earlier stage data = dict(setup="email") with capture_flashes() as flashes: response = client.post("/tf-setup", data=data) assert response.status_code in [302, 303] assert flashes[0]["category"] == "error" assert flashes[0]["message"].encode("utf-8") == get_message( "TWO_FACTOR_PERMISSION_DENIED" ) @pytest.mark.settings(two_factor_required=True, url_prefix="/api") def test_two_factor_illegal_state(app, client, get_message): # trying to pick method without doing earlier stage data = dict(setup="email") with capture_flashes() as flashes: response = client.post("/api/tf-setup", data=data) assert response.status_code in [302, 303] assert "/api/login" in response.location assert flashes[0]["category"] == "error" assert flashes[0]["message"].encode("utf-8") == get_message( "TWO_FACTOR_PERMISSION_DENIED" ) # try validate code response = client.post( "/api/tf-validate", data=dict(code=b"333"), follow_redirects=False ) assert response.status_code in [302, 303] assert "/api/login" in response.location # try rescue response = client.post( "/api/tf-rescue", data=dict(help_setup="lost_device"), follow_redirects=False ) assert response.status_code in [302, 303] assert "/api/login" in response.location @pytest.mark.settings(two_factor_required=True) def test_two_factor_flag(app, clients, get_message, outbox, signals): # trying to verify code without going through two-factor # first login function client = clients wrong_code = b"000000" response = client.post( "/tf-validate", data=dict(code=wrong_code), follow_redirects=True ) message = b"You currently do not have permissions to access this page" assert message in response.data # Test login using invalid email data = dict(email="nobody@lp.com", password="password") response = client.post("/login", data=data, follow_redirects=True) assert b"Specified user does not exist" in response.data response = client.post( "/login", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) assert b"Specified user does not exist" in response.data # Test login using valid email and invalid password data = dict(email="gal@lp.com", password="wrong_pass") response = client.post("/login", data=data, follow_redirects=True) assert b"Invalid password" in response.data response = client.post( "/login", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) assert b"Invalid password" in response.data # Test two-factor authentication first login data = dict(email="matt@lp.com", password="password") response = client.post("/login", data=data, follow_redirects=True) message = b"Two-Factor authentication adds an extra layer of security" assert message in response.data response = client.post( "/tf-setup", data=dict(setup="not_a_method"), follow_redirects=True ) assert b"Marked method is not valid" in response.data with client.session_transaction() as session: assert session["tf_state"] == "setup_from_login" # try non-existing setup on setup page (using json) data = dict(setup="not_a_method") response = client.post( "/tf-setup", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) assert response.status_code == 400 assert ( response.json["response"]["field_errors"]["setup"][0] == "Marked method is not valid" ) data = dict(setup="email") response = client.post( "/tf-setup", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) # Test for sms in process of valid login sms_sender = SmsSenderFactory.createSender("test") data = dict(email="gal@lp.com", password="password") response = client.post( "/login", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) assert b'"code": 200' in response.data assert sms_sender.get_count() == 1 session = get_session(response) assert session["tf_state"] == "ready" code = sms_sender.messages[0].split()[-1] # submit bad token to two_factor_token_validation response = client.post("/tf-validate", data=dict(code=wrong_code)) assert get_message("TWO_FACTOR_INVALID_TOKEN") in response.data assert ( signals["user_failed_authn"][2]["request_endpoint"] == "security.two_factor_token_validation" ) assert signals["user_failed_authn"][2]["user"].email == "gal@lp.com" assert signals["user_failed_authn"][2]["auth_type"] == "code" assert signals["user_failed_authn"][2]["tfa"] # submit right token and show appropriate response response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") in response.data # Upon completion, session cookie shouldn't have any two factor stuff in it. assert not tf_in_session(get_session(response)) # Test change two_factor view from sms to mail setup_data = dict(setup="email") response = client.post("/tf-setup", data=setup_data, follow_redirects=True) msg = b"Enter code to complete setup" assert msg in response.data # Fetch token validate form response = client.get("/tf-validate") assert response.status_code == 200 # make sure two_factor_verify_code_form is set assert b'name="code"' in response.data matcher = re.match(r".*code: ([0-9]+).*", outbox[1].body, re.IGNORECASE | re.DOTALL) # submit right token and show appropriate response response = client.post( "/tf-validate", data=dict(code=matcher.group(1)), follow_redirects=True ) assert b"You successfully changed your two-factor method" in response.data # Test change two_factor password confirmation view to authenticator # Setup authenticator setup_data = dict(setup="authenticator") response = client.post("/tf-setup", data=setup_data, follow_redirects=True) assert b"Open an authenticator app on your device" in response.data # verify png QRcode is present assert b"data:image/svg+xml;base64," in response.data # parse out key rd = response.data.decode("utf-8") matcher = re.match(r".*((?:\S{4}-){7}\S{4}).*", rd, re.DOTALL) totp_secret = matcher.group(1) # Generate token from passed totp_secret and confirm setup totp = TOTP(totp_secret) code = totp.generate().token response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"You successfully changed your two-factor method" in response.data logout(client) # Test login with remember_token assert not client.get_cookie("remember_token") data = dict(email="gal@lp.com", password="password", remember=True) response = client.post( "/login", json=data, headers={"Content-Type": "application/json"}, follow_redirects=True, ) # Generate token from passed totp_secret code = totp.generate().token response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") in response.data # Verify that the remember token is properly set assert client.get_cookie("remember_token") response = logout(client) # Verify that logout clears session info assert not tf_in_session(get_session(response)) # Test two-factor authentication first login data = dict(email="matt@lp.com", password="password") response = client.post("/login", data=data, follow_redirects=True) message = b"Two-Factor authentication adds an extra layer of security" assert message in response.data # check availability of qrcode when this option is not picked assert b"data:image/png;base64," not in response.data # check availability of qrcode page when this option is picked setup_data = dict(setup="authenticator") response = client.post("/tf-setup", data=setup_data, follow_redirects=True) assert b"Open an authenticator app on your device" in response.data assert b"data:image/svg+xml;base64," in response.data # check appearance of setup page when sms picked and phone number entered sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"Enter code to complete setup" in response.data assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") in response.data assert not tf_in_session(get_session(response)) logout(client) # check when two_factor_rescue function should not appear rescue_data_json = dict(help_setup="lost_device") response = client.post( "/tf-rescue", json=rescue_data_json, headers={"Content-Type": "application/json"}, ) assert b'"code": 400' in response.data # check when two_factor_rescue function should appear data = dict(email="gal2@lp.com", password="password") response = client.post("/login", data=data, follow_redirects=True) assert b"Please enter your authentication code" in response.data rescue_data = dict(help_setup="email") response = client.post("/tf-rescue", data=rescue_data, follow_redirects=True) message = b"The code for authentication was sent to your email address" assert message in response.data rescue_data = dict(help_setup="help") response = client.post("/tf-rescue", data=rescue_data, follow_redirects=True) message = b"An email was sent to us in order to reset your application account" assert message in response.data @pytest.mark.settings(two_factor_rescue_email=False) def test_no_rescue_email(app, client): headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.post( "/login", json=dict(email="gal2@lp.com", password="password") ) assert response.json["response"]["tf_required"] response = client.get("/tf-rescue", headers=headers) options = response.json["response"]["recovery_options"] assert len(options.keys()) == 1 assert "help" in options.keys() # make sure that even if post using email - we get an error response = client.post("/tf-rescue", json=dict(help_setup="email")) assert response.status_code == 400 assert ( response.json["response"]["field_errors"]["help_setup"][0] == "Not a valid choice." ) @pytest.mark.settings(two_factor_required=True) def test_setup_bad_phone(app, client, get_message): data = dict(email="matt@lp.com", password="password") response = client.post("/login", data=data, follow_redirects=True) message = b"Two-Factor authentication adds an extra layer of security" assert message in response.data sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="555-1212") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"Phone number not valid" in response.data assert sms_sender.get_count() == 0 # require phone number with SMS data = dict(setup="sms") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"Phone number not valid" in response.data assert sms_sender.get_count() == 0 # Now setup good phone response = client.post( "/tf-setup", data=dict(setup="sms", phone="650-555-1212"), follow_redirects=True ) assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] # shouldn't get authenticator stuff when setting up SMS assert b"data:image/png;base64," not in response.data response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") in response.data assert not tf_in_session(get_session(response)) headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.get("/tf-setup", headers=headers) # N.B. right now for tfa - we don't canonicalize phone number (since user # never has to type it in). assert response.json["response"]["tf_phone_number"] == "650-555-1212" @pytest.mark.settings(two_factor_required=True) def test_json(app, client, signals): """ Test login/setup using JSON. """ headers = {"Accept": "application/json", "Content-Type": "application/json"} # Login with someone already setup. sms_sender = SmsSenderFactory.createSender("test") data = dict(email="gal@lp.com", password="password") response = client.post("/login", json=data, headers=headers) assert response.status_code == 200 assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "ready" assert response.json["response"]["tf_primary_method"] == "sms" # Verify SMS sent assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] assert signals["tf_security_token_sent"][0]["token"] == code assert signals["tf_security_token_sent"][0]["method"] == "sms" response = client.post("/tf-validate", json=dict(code=code)) assert response.status_code == 200 assert signals["tf_code_confirmed"][0]["user_email"] == "gal@lp.com" # verify logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 logout(client) # Test that user not yet setup for 2FA gets correct response. data = dict(email="matt@lp.com", password="password") response = client.post("/login", json=data) assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "setup_from_login" # Start setup process. response = client.get("/tf-setup", headers=headers) assert response.json["response"]["tf_required"] assert "sms" in response.json["response"]["tf_available_methods"] # Now try to setup data = dict(setup="sms", phone="+442083661177") response = client.post("/tf-setup", json=data) assert response.status_code == 200 assert response.json["response"]["tf_state"] == "validating_profile" assert response.json["response"]["tf_primary_method"] == "sms" code = sms_sender.messages[0].split()[-1] assert signals["tf_security_token_sent"][1]["token"] == code response = client.post("/tf-validate", json=dict(code=code), headers=headers) assert response.status_code == 200 assert "csrf_token" in response.json["response"] assert response.json["response"]["user"]["email"] == "matt@lp.com" logout(client) # Verify tf is now setup and can directly get code data = dict(email="matt@lp.com", password="password") response = client.post("/login", json=data) assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "ready" # send bad code response = client.post("/tf-validate", json=dict(code="whatsup")) assert response.status_code == 400 assert response.json["response"]["field_errors"]["code"][0] == "Invalid code" assert response.json["response"]["errors"][0] == "Invalid code" assert ( signals["user_failed_authn"][0]["request_endpoint"] == "security.two_factor_token_validation" ) assert signals["user_failed_authn"][0]["user"].email == "matt@lp.com" assert signals["user_failed_authn"][0]["auth_type"] == "code" assert signals["user_failed_authn"][0]["tfa"] # Do a GET - should get recovery options response = client.get("/tf-validate", headers=headers) options = response.json["response"]["recovery_options"] assert "email" in options.keys() assert "/tf-rescue" in options["email"] # now send correct code code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", json=dict(code=code)) assert response.status_code == 200 # verify logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 # tf-setup should provide existing info response = client.get("/tf-setup", headers=headers) assert response.json["response"]["tf_required"] assert "sms" in response.json["response"]["tf_available_methods"] assert "disable" not in response.json["response"]["tf_available_methods"] assert response.json["response"]["tf_primary_method"] == "sms" assert response.json["response"]["tf_phone_number"] == "+442083661177" assert not tf_in_session(get_session(response)) check_signals( signals, {"user_failed_authn": 1, "tf_security_token_sent": 3, "tf_code_confirmed": 3}, ) @pytest.mark.settings(two_factor_rescue_mail="helpme@myapp.com") def test_rescue_json(app, client, outbox): # it's an error if not primary authenticated rescue_data_json = dict(help_setup="help") response = client.post( "/tf-rescue", json=rescue_data_json, ) assert response.status_code == 400 # check when two_factor_rescue function should appear data = dict(email="gal2@lp.com", password="password") response = client.post("/login", json=data) assert response.json["response"]["tf_required"] rescue_data = dict(help_setup="email") response = client.post("/tf-rescue", json=rescue_data) assert response.status_code == 200 assert outbox[0].recipients == ["gal2@lp.com"] assert outbox[0].sender == "no-reply@localhost" assert outbox[0].subject == "Two-Factor Login" assert "This code will expire in 5 minutes" in outbox[0].body matcher = re.match(r".*code: ([0-9]+).*", outbox[0].body, re.IGNORECASE | re.DOTALL) response = client.post("/tf-validate", json=dict(code=matcher.group(1))) assert response.status_code == 200 logout(client) # Try rescue with no email (should send email to admin) client.post("/login", json=data) rescue_data = dict(help_setup="help") response = client.post("/tf-rescue", json=rescue_data) assert response.status_code == 200 assert outbox[1].recipients == ["helpme@myapp.com"] assert outbox[1].sender == "no-reply@localhost" assert outbox[1].subject == "Two-Factor Rescue" assert "gal2@lp.com" in outbox[1].body @pytest.mark.settings(two_factor_required=True) def test_json_auth_token(app, client): """ Test getting auth_token with two-factor """ headers = {"Accept": "application/json", "Content-Type": "application/json"} # Login with someone already setup. sms_sender = SmsSenderFactory.createSender("test") data = dict(email="gal@lp.com", password="password") response = client.post("/login", json=data, headers=headers) assert response.status_code == 200 assert response.json["response"]["tf_required"] # Verify SMS sent assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate?include_auth_token", json=dict(code=code)) assert response.status_code == 200 token = response.json["response"]["user"]["authentication_token"] headers = {"Authentication-Token": token} # make sure can access restricted page response = client.get("/token", headers=headers) assert b"Token Authentication" in response.data @pytest.mark.settings(two_factor_required=True) def test_no_opt_out(app, client, get_message): # Test if 2FA required, can't opt-out. sms_sender = SmsSenderFactory.createSender("test") response = client.post( "/login", data=dict(email="gal@lp.com", password="password"), follow_redirects=True, ) assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] # submit right token and show appropriate response response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") in response.data response = client.get("/tf-setup", follow_redirects=True) assert b"Disable two factor" not in response.data assert b"Currently setup two-factor method: SMS" in response.data # Try to opt-out data = dict(setup="disable") response = client.post("/tf-setup", data=data, follow_redirects=True) assert response.status_code == 200 assert b"Marked method is not valid" in response.data @pytest.mark.settings( two_factor_setup_url="/custom-setup", two_factor_rescue_url="/custom-rescue" ) def test_custom_urls(client): response = client.get("/tf-setup") assert response.status_code == 404 response = client.get("/custom-setup") assert response.status_code in [302, 303] response = client.get("/custom-rescue") assert response.status_code in [302, 303] def test_evil_validate(app, client): """ Test logged in, and randomly try to validate a token """ signalled_identity = [] @identity_changed.connect_via(app) def on_identity_changed(app, identity): signalled_identity.append(identity.id) response = authenticate(client, "jill@lp.com") session = get_session(response) assert "tf_state" not in session with app.app_context(): user = app.security.datastore.find_user(email="jill@lp.com") assert signalled_identity[0] == user.fs_uniquifier del signalled_identity[:] # try to validate response = client.post("/tf-validate", data=dict(code="?"), follow_redirects=True) # This should log us out since it thinks we are evil assert not signalled_identity[0] del signalled_identity[:] def test_opt_in(app, client, get_message): """ Test entire lifecycle of user not having 2FA - setting it up, then deciding to turn it back off All using forms based API """ signalled_identity = [] @identity_changed.connect_via(app) def on_identity_changed(app, identity): signalled_identity.append(identity.id) response = authenticate(client, "jill@lp.com") session = get_session(response) assert "tf_state" not in session with app.app_context(): user = app.security.datastore.find_user(email="jill@lp.com") assert signalled_identity[0] == user.fs_uniquifier del signalled_identity[:] # opt-in for SMS 2FA sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"Enter code to complete setup" in response.data assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] # Validate token - this should complete 2FA setup response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"You successfully changed" in response.data assert check_location(app, response.history[0].location, "/tf-setup") # Upon completion, session cookie shouldnt have any two factor stuff in it. session = get_session(response) assert not tf_in_session(session) # Log out logout(client) assert not signalled_identity[0] del signalled_identity[:] # Login now should require 2FA with sms sms_sender = SmsSenderFactory.createSender("test") response = authenticate(client, "jill@lp.com") session = get_session(response) assert session["tf_state"] == "ready" assert len(signalled_identity) == 0 assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") in response.data # Verify now logged in with app.app_context(): user = app.security.datastore.find_user(email="jill@lp.com") assert signalled_identity[0] == user.fs_uniquifier del signalled_identity[:] # Now opt back out. data = dict(setup="disable") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"You successfully disabled two-factor authorization." in response.data # Log out logout(client) assert not signalled_identity[0] del signalled_identity[:] # Should be able to log in with just user/pass response = authenticate(client, "jill@lp.com") session = get_session(response) assert "tf_state" not in session with app.app_context(): user = app.security.datastore.find_user(email="jill@lp.com") assert signalled_identity[0] == user.fs_uniquifier def test_opt_in_nc(app, client_nc, get_message, signals): """ Test tf-setup without cookies """ response = json_authenticate(client_nc, "jill@lp.com") assert response.status_code == 200 token = response.json["response"]["user"]["authentication_token"] headers = {"Authentication-Token": token, "Accept": "application/json"} sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") response = client_nc.post("/tf-setup", json=data, headers=headers) assert response.status_code == 200 state_token = response.json["response"]["tf_state_token"] assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] # send bad token response = client_nc.post( "/tf-setup/not-a-token", json=dict(code=code), headers=headers ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) # send bad code response = client_nc.post( f"/tf-setup/{state_token}", json=dict(code=12345), headers=headers ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "TWO_FACTOR_INVALID_TOKEN" ) # setup shouldn't trigger this. assert len(signals["user_failed_authn"]) == 0 # Validate token - this should complete 2FA setup @tf_profile_changed.connect_via(app) def pc(sender, user, method, **kwargs): assert method == "sms" assert user.tf_phone_number == "+442083661177" response = client_nc.post( f"/tf-setup/{state_token}", json=dict(code=code), headers=headers ) assert response.status_code == 200 response = client_nc.get("/tf-setup", headers=headers) assert response.json["response"]["tf_method"] == "sms" assert response.json["response"]["tf_phone_number"] == "+442083661177" def test_opt_in_nc_non_string_code(app, client_nc): """ Test tf-setup with dict code. It should be rejected cleanly by the IsStringOrInt validator to prevent a passlib crash. """ response = json_authenticate(client_nc, "jill@lp.com") token = response.json["response"]["user"]["authentication_token"] headers = {"Authentication-Token": token, "Accept": "application/json"} SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") response = client_nc.post("/tf-setup", json=data, headers=headers) state_token = response.json["response"]["tf_state_token"] response = client_nc.post( f"/tf-setup/{state_token}", json=dict(code={"not": "a code"}), headers=headers ) assert response.status_code == 400 @pytest.mark.settings(token_max_age=timedelta(days=4)) def test_opt_in_nc_expired(app, client_nc, get_message): """ Test tf-setup without cookies - expired token """ with freeze_time( date.today() + timedelta(days=-1) ): # older than TWO_FACTOR_SETUP_WITHIN response = json_authenticate(client_nc, "jill@lp.com") assert response.status_code == 200 token = response.json["response"]["user"]["authentication_token"] headers = {"Authentication-Token": token, "Accept": "application/json"} sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") response = client_nc.post("/tf-setup", json=data, headers=headers) assert response.status_code == 200 state_token = response.json["response"]["tf_state_token"] assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] # Validate token - this should complete 2FA setup response = client_nc.post( f"/tf-setup/{state_token}", json=dict(code=code), headers=headers ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "TWO_FACTOR_SETUP_EXPIRED", within="30 minutes" ) def test_opt_in_state_token(app, client, get_message, signals): """ Test using forms and new state_token approach (rather than sessions to store intermediate state) """ authenticate(client, "jill@lp.com") # opt-in for SMS 2FA sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"Enter code to complete setup" in response.data assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] verify_url = get_form_action(response, 1) # this will be with state_token # send in bad token response = client.post( "/tf-setup/not-a-token", data=dict(code=code), follow_redirects=True ) assert check_location(app, response.history[0].location, "/tf-setup") assert get_message("API_ERROR") in response.data # send in bad code response = client.post(verify_url, data=dict(code=12345), follow_redirects=True) assert check_location(app, response.history[0].location, "/tf-setup") assert get_message("TWO_FACTOR_INVALID_TOKEN") in response.data # setup shouldn't trigger this. assert len(signals["user_failed_authn"]) == 0 # Validate token - this should complete 2FA setup response = client.post(verify_url, data=dict(code=code), follow_redirects=True) assert b"You successfully changed" in response.data assert check_location(app, response.history[0].location, "/tf-setup") # Upon completion, session cookie shouldn't have any two factor stuff in it. session = get_session(response) assert not tf_in_session(session) response = client.get("/tf-setup") assert b"Disable two-factor" in response.data assert b"Currently setup two-factor method: SMS" in response.data def test_opt_out_json(app, client, get_message): headers = {"Accept": "application/json", "Content-Type": "application/json"} tf_authenticate(app, client) response = client.get("tf-setup", headers=headers) assert "disable" in response.json["response"]["tf_available_methods"] response = client.post("tf-setup", json=dict(setup="disable"), headers=headers) assert response.status_code == 200 logout(client) # Should be able to log in with just user/pass response = authenticate(client, "gal@lp.com") session = get_session(response) assert "tf_state" not in session # verify logged in assert is_authenticated(client, get_message) response = client.get("tf-setup", headers=headers) assert "disable" not in response.json["response"]["tf_available_methods"] @pytest.mark.filterwarnings("ignore") @pytest.mark.recoverable() @pytest.mark.settings(two_factor_required=True, auto_login_after_reset=True) def test_recoverable(app, client, get_message): # make sure 'forgot password' doesn't bypass 2FA. # 'gal@lp.com' already setup for SMS rtokens = [] sms_sender = SmsSenderFactory.createSender("test") @reset_password_instructions_sent.connect_via(app) def on_instructions_sent(sapp, **kwargs): rtokens.append(kwargs["token"]) client.post("/reset", data=dict(email="gal@lp.com"), follow_redirects=True) response = client.post( "/reset/" + rtokens[0], data={"password": "awesome sunset", "password_confirm": "awesome sunset"}, follow_redirects=True, ) # Should have redirected us to the 2FA login page assert b"Please enter your authentication code" in response.data # we shouldn't be logged in response = client.get("/profile", follow_redirects=False) assert response.status_code in [302, 303] # Grab code that was sent assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") in response.data # verify we are logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.settings(two_factor_required=True) def test_admin_setup_reset(app, client, get_message): # Verify can use administrative datastore method to setup SMS # and that administrative reset removes access. sms_sender = SmsSenderFactory.createSender("test") data = dict(email="gene@lp.com", password="password") response = client.post( "/login", json=data, headers={"Content-Type": "application/json"} ) assert response.json["response"]["tf_required"] # we shouldn't be logged in response = client.get("/profile", follow_redirects=False) assert response.status_code in [302, 303] assert response.location == "/login?next=/profile" # Use admin to setup gene's SMS/phone. with app.app_context(): user = app.security.datastore.find_user(email="gene@lp.com") totp_secret = app.security._totp_factory.generate_totp_secret() app.security.datastore.tf_set(user, "sms", totp_secret, phone="+442083661177") app.security.datastore.commit() response = authenticate(client, "gene@lp.com") session = get_session(response) assert session["tf_state"] == "ready" # Grab code that was sent assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") in response.data # verify we are logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 # logout logout(client) # use administrative reset method with app.app_context(): user = app.security.datastore.find_user(email="gene@lp.com") app.security.datastore.reset_user_access(user) app.security.datastore.commit() data = dict(email="gene@lp.com", password="password") response = client.post( "/login", json=data, headers={"Content-Type": "application/json"} ) assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "setup_from_login" # we shouldn't be logged in response = client.get("/profile", follow_redirects=False) assert response.status_code in [302, 303] @pytest.mark.settings(two_factor_required=True) def test_datastore(app, clients, get_message): # Test that user record is properly set after proper 2FA setup. client = clients sms_sender = SmsSenderFactory.createSender("test") data = dict(email="gene@lp.com", password="password") response = client.post( "/login", json=data, headers={"Content-Type": "application/json"} ) assert response.json["meta"]["code"] == 200 session = get_session(response) assert session["tf_state"] == "setup_from_login" # setup data = dict(setup="sms", phone="+442083661177") response = client.post( "/tf-setup", json=data, headers={"Content-Type": "application/json"} ) assert sms_sender.get_count() == 1 session = get_session(response) assert session["tf_state"] == "validating_profile" assert session["tf_primary_method"] == "sms" code = sms_sender.messages[0].split()[-1] # submit token and show appropriate response response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") in response.data session = get_session(response) # Verify that successful login clears session info assert not tf_in_session(session) with app.app_context(): user = app.security.datastore.find_user(email="gene@lp.com") assert user.tf_primary_method == "sms" assert user.tf_phone_number == "+442083661177" assert "enckey" in user.tf_totp_secret def test_totp_secret_generation(app, client): """ Test the totp secret generation upon changing method to make sure it stays the same after the process is completed """ # Properly log in jill for this test signalled_identity = [] @identity_changed.connect_via(app) def on_identity_changed(app, identity): signalled_identity.append(identity.id) response = authenticate(client, "jill@lp.com") session = get_session(response) assert "tf_state" not in session with app.app_context(): user = app.security.datastore.find_user(email="jill@lp.com") assert signalled_identity[0] == user.fs_uniquifier assert not user.tf_totp_secret del signalled_identity[:] sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661188") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"Enter code to complete setup" in response.data assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] # Retrieve the currently generated totp secret for later comparison session = get_session(response) generated_secret = session["tf_totp_secret"] assert "enckey" in generated_secret # Validate token - this should complete 2FA setup response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert b"You successfully changed" in response.data # Retrieve the final totp secret and make sure it matches the previous one with app.app_context(): user = app.security.datastore.find_user(email="jill@lp.com") assert generated_secret == user.tf_totp_secret # Finally opt back out and check that tf_totp_secret is None data = dict(setup="disable") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"You successfully disabled two-factor authorization." in response.data with app.app_context(): user = app.security.datastore.find_user(email="jill@lp.com") assert user.tf_totp_secret is None # Log out logout(client) assert not signalled_identity[0] del signalled_identity[:] @pytest.mark.settings(two_factor_enabled_methods=["authenticator"]) def test_just_authenticator(app, client): authenticate(client, email="jill@lp.com") response = client.get("/tf-setup", follow_redirects=True) assert b"Set up using SMS" not in response.data data = dict(setup="authenticator") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"Submit Code" in response.data # test json response = client.post("/tf-setup", json=data) assert response.status_code == 200 @pytest.mark.settings( USER_IDENTITY_ATTRIBUTES=[ {"username": {"mapper": lambda x: "@" not in x}}, {"email": {"mapper": uia_email_mapper}}, ] ) def test_authr_identity(app, client): # Setup authenticator headers = {"Accept": "application/json", "Content-Type": "application/json"} authenticate(client, email="jill@lp.com") setup_data = dict(setup="authenticator") response = client.post("/tf-setup", json=setup_data, headers=headers) jr = response.json["response"] assert jr["tf_authr_issuer"] == "tests" assert jr["tf_authr_username"] == "jill" assert jr["tf_state"] == "validating_profile" assert len(jr["tf_authr_b32key"]) % 8 == 0 and re.match( r"^[A-Z2-7]+=*$", jr["tf_authr_b32key"] ) assert "tf_authr_key" in response.json["response"] assert ( jr["tf_authr_uri"] == f"otpauth://totp/tests:jill?secret={jr['tf_authr_b32key']}&issuer=tests" ) @pytest.mark.settings( USER_IDENTITY_ATTRIBUTES=[ {"security_number": {"mapper": lambda x: x.isdigit()}}, {"email": {"mapper": uia_email_mapper}}, ] ) def test_authr_identity_num(app, client): # Test that response to setup has 'security_number' as the 'username' # since it is listed first. headers = {"Accept": "application/json", "Content-Type": "application/json"} authenticate(client, email="jill@lp.com") setup_data = dict(setup="authenticator") response = client.post("/tf-setup", json=setup_data, headers=headers) assert response.json["response"]["tf_authr_username"] == "456789" assert "tf_authr_key" in response.json["response"] @pytest.mark.settings( USER_IDENTITY_ATTRIBUTES=[ {"email": {"mapper": uia_email_mapper}}, {"username": {"mapper": lambda x: x}}, ] ) def test_email_salutation(app, client, outbox): authenticate(client, email="jill@lp.com") response = client.post("/tf-setup", data=dict(setup="email"), follow_redirects=True) msg = b"Enter code to complete setup" assert msg in response.data assert "jill@lp.com" in outbox[0].recipients assert "jill@lp.com" in outbox[0].body assert "jill@lp.com" in outbox[0].alts["html"] @pytest.mark.settings( USER_IDENTITY_ATTRIBUTES=[ {"username": {"mapper": lambda x: "@" not in x}}, {"email": {"mapper": uia_email_mapper}}, ] ) def test_username_salutation(app, client, outbox): authenticate(client, email="jill@lp.com") response = client.post("/tf-setup", data=dict(setup="email"), follow_redirects=True) msg = b"Enter code to complete setup" assert msg in response.data assert "jill@lp.com" in outbox[0].recipients assert "jill@lp.com" not in outbox[0].body assert "jill@lp.com" not in outbox[0].alts["html"] assert "jill" in outbox[0].body @pytest.mark.settings(sms_service="bad") def test_bad_sender(app, client, get_message): # If SMS sender fails - make sure propagated # Test form, json, x signin, setup headers = {"Accept": "application/json", "Content-Type": "application/json"} # test normal, already setup up login. with capture_flashes() as flashes: data = {"email": "gal@lp.com", "password": "password"} response = client.post("login", data=data, follow_redirects=False) assert response.status_code in [302, 303] assert "/login" in response.location assert get_message("FAILED_TO_SEND_CODE") in flashes[0]["message"].encode("utf-8") # test w/ JSON data = dict(email="gal@lp.com", password="password") response = client.post("login", json=data, headers=headers) assert response.status_code == 500 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "FAILED_TO_SEND_CODE" ) # Now test setup tf_authenticate(app, client) data = dict(setup="sms", phone="+442083661188") response = client.post("tf-setup", data=data) assert get_message("FAILED_TO_SEND_CODE") in response.data response = client.post("tf-setup", json=data, headers=headers) assert response.status_code == 500 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "FAILED_TO_SEND_CODE" ) cresponse = [None, "That didnt work out as we planned", "Failed Again"] def _tf_send_code(self, method, totp_secret, phone_number): return self.code_response.pop(0) @pytest.mark.app_settings( TESTING_USER_INJECT=dict( tf_send_security_token=_tf_send_code, code_response=cresponse ) ) def test_replace_send_code(app, client, get_message): data = dict(email="gal@lp.com", password="password") response = client.post("/login", data=data, follow_redirects=True) assert b"Please enter your authentication code" in response.data rescue_data = dict(help_setup="email") response = client.post("/tf-rescue", data=rescue_data, follow_redirects=True) assert b"That didnt work out as we planned" in response.data # Test JSON headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.post("/tf-rescue", json=rescue_data, headers=headers) assert response.status_code == 500 assert response.json["response"]["field_errors"]["help_setup"][0] == "Failed Again" def test_propagate_next(app, client): # verify we propagate the ?next param all the way through a two-factor login with capture_send_code_requests() as codes: data = dict(email="gal@lp.com", password="password") response = client.post("/login?next=/im-in", data=data, follow_redirects=True) assert "?next=/im-in" in response.request.url # grab URL from form to show that our template propagates ?next verify_url = get_form_action(response) response = client.post( verify_url, data=dict(code=codes[0]["login_token"]), follow_redirects=False ) assert "/im-in" in response.location logout(client) # do it with next in the form data = dict(email="gal@lp.com", password="password", next="/im-in") response = client.post("/login", data=data, follow_redirects=True) assert "?next=/im-in" in response.request.url # grab URL from form to show that our template propagates ?next verify_url = get_form_action(response) response = client.post( verify_url, data=dict(code=codes[1]["login_token"]), follow_redirects=False ) assert "/im-in" in response.location @pytest.mark.settings(freshness=timedelta(minutes=0)) def test_verify(app, client, get_message): # Test setup when reauthenticate required authenticate(client) response = client.get("tf-setup", follow_redirects=False) assert check_location(app, response.location, "/verify?next=/tf-setup") logout(client) # Now try again - follow redirects to get to verify form # This call should require re-verify authenticate(client) response = client.get("tf-setup", follow_redirects=True) assert get_message("REAUTHENTICATION_REQUIRED") in response.data verify_password_url = get_form_action(response) # Send wrong password response = client.post( verify_password_url, data=dict(password="iforgot"), follow_redirects=True, ) assert response.status_code == 200 assert get_message("INVALID_PASSWORD") in response.data # Verify with correct password with capture_flashes() as flashes: response = client.post( verify_password_url, data=dict(password="password"), follow_redirects=False, ) assert response.status_code in [302, 303] assert check_location(app, response.location, "/tf-setup") assert get_message("REAUTHENTICATION_SUCCESSFUL") == flashes[0]["message"].encode( "utf-8" ) def test_verify_json(app, client, get_message): # Test setup when reauthenticate required # N.B. with freshness=0 we never set a grace period and should never be able to # get to /tf-setup authenticate(client) headers = {"Accept": "application/json", "Content-Type": "application/json"} app.config["SECURITY_FRESHNESS"] = timedelta(minutes=0) response = client.get("tf-setup", headers=headers) assert response.status_code == 401 assert response.json["response"]["reauth_required"] response = client.post("verify", json=dict(password="notmine"), headers=headers) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_PASSWORD" ) response = client.post("verify", json=dict(password="password"), headers=headers) assert response.status_code == 200 app.config["SECURITY_FRESHNESS"] = timedelta(minutes=60) response = client.get("tf-setup", headers=headers) assert response.status_code == 200 @pytest.mark.settings(freshness=timedelta(minutes=-1)) def test_setup_nofresh(app, client, get_message): authenticate(client) response = client.get("tf-setup", follow_redirects=False) assert response.status_code == 200 @pytest.mark.settings(two_factor_enabled_methods=["email"]) def test_no_sms(app, get_message, outbox): pytest.importorskip("sqlalchemy") pytest.importorskip("flask_sqlalchemy") # Make sure that don't require tf_phone_number if SMS isn't an option. from sqlalchemy import ( Boolean, Column, Integer, String, ) from sqlalchemy.orm import relationship, backref from flask_sqlalchemy import SQLAlchemy from flask_security.models import fsqla_v2 as fsqla from flask_security import Security, UserMixin, hash_password app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" db = SQLAlchemy(app) fsqla.FsModels.set_db_info(db) class Role(db.Model, fsqla.FsRoleMixin): pass class User(db.Model, UserMixin): id = Column(Integer, primary_key=True) email = Column(String(255), unique=True, nullable=False) password = Column(String(255), nullable=False) active = Column(Boolean(), nullable=False) # Faster token checking fs_uniquifier = Column(String(64), unique=True, nullable=False) # 2FA tf_primary_method = Column(String(64), nullable=True) tf_totp_secret = Column(String(255), nullable=True) roles = relationship( "Role", secondary="roles_users", backref=backref("users", lazy="dynamic") ) with app.app_context(): db.create_all() ds = SQLAlchemyUserDatastore(db, User, Role) app.security = Security(app, datastore=ds) with app.app_context(): ds.create_user( email="trp@lp.com", password=hash_password("password"), ) ds.commit() client = app.test_client() data = dict(email="trp@lp.com", password="password") client.post("/login", data=data, follow_redirects=True) response = client.post("/tf-setup", data=dict(setup="email"), follow_redirects=True) msg = b"Enter code to complete setup" assert msg in response.data matcher = re.match(r".*code: ([0-9]+).*", outbox[0].body, re.IGNORECASE | re.DOTALL) # submit right token and show appropriate response response = client.post( "/tf-validate", data=dict(code=matcher.group(1)), follow_redirects=True ) assert b"You successfully changed your two-factor method" in response.data with app.app_context(): db.engine.dispose() @pytest.mark.settings(two_factor_post_setup_view="/post_setup_view") def test_post_setup_redirect(app, client): authenticate(client, "jill@lp.com") sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"Enter code to complete setup" in response.data assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] # Validate token - this should complete 2FA setup response = client.post("/tf-validate", data=dict(code=code), follow_redirects=False) assert check_location(app, response.location, "/post_setup_view") @pytest.mark.babel(babel_default_locale="cic_US", test_xlations=True) def test_xlation(app, client, get_message_local): # Test method translation # login as gal2 which has 'authenticator' set up response = authenticate(client, email="gal2@lp.com", follow_redirects=True) with app.test_request_context(): existing = localize_callback( "Please enter your authentication code generated via: %(method)s", method="authenticator", ) assert "OKAY!" in existing assert markupsafe.escape(existing).encode() in response.data with app.app_context(): # generate 'code' as authenticator would and complete authentication user = app.security.datastore.find_user(email="gal2@lp.com") code = app.security._totp_factory.generate_totp_password(user.tf_totp_secret) client.post("/tf-validate", data=dict(code=code), follow_redirects=True) response = client.get("/tf-setup", follow_redirects=True) with app.test_request_context(): existing = localize_callback( "Currently setup two-factor method: %(method)s", method="authenticator" ) assert "OKAY!" in existing assert markupsafe.escape(existing).encode() in response.data @pytest.mark.csrf(ignore_unauth=True) @pytest.mark.settings(two_factor_post_setup_view="/post_setup_view") def test_setup_csrf(app, client): # Verify /tf-setup properly handles CSRF and template relays CSRF errors tf_authenticate(app, client) response = client.get("tf-setup") assert b"Disable" in response.data csrf_token = get_form_input_value(response, "csrf_token") response = client.post("tf-setup", data=dict(setup="disable")) assert b"The CSRF token is missing" in response.data response = client.post( "tf-setup", data=dict(setup="disable", csrf_token=csrf_token) ) assert check_location(app, response.location, "/post_setup_view") @pytest.mark.csrf(ignore_unauth=True, csrfprotect=True) def test_setup_csrf_header(app, client): # Test that can setup using csrf token in header tf_authenticate(app, client) response = client.get("tf-setup", json=dict()) csrf_token = response.json["response"]["csrf_token"] response = client.post("tf-setup", json=dict(setup="disable")) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." response = client.post( "tf-setup", json=dict(setup="disable"), headers={"X-CSRF-Token": csrf_token} ) assert response.status_code == 200 @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings(CSRF_COOKIE_NAME="XSRF-Token") def test_csrf_2fa_login_cookie(app, client): # Use XSRF-Token cookie for entire login sequence sms_sender = SmsSenderFactory.createSender("test") response = client.get( "/login", data={}, headers={"Content-Type": "application/json"} ) assert client.get_cookie("XSRF-Token") csrf_token = response.json["response"]["csrf_token"] assert csrf_token == client.get_cookie("XSRF-Token").value response = client.post( "/login", json=dict(email="gal@lp.com", password="password"), headers={ "Content-Type": "application/json", "X-CSRF-Token": client.get_cookie("XSRF-Token").value, }, ) assert b'"code": 200' in response.data session = get_session(response) assert session["tf_state"] == "ready" assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post( "/tf-validate", json=dict(code=code), headers={ "Content-Type": "application/json", "X-CSRF-Token": client.get_cookie("XSRF-Token").value, }, ) assert response.status_code == 200 # verify original session csrf_token still works. response = client.post( "/json_auth", json=dict(label="label"), headers={"Content-Type": "application/json", "X-CSRF-Token": csrf_token}, ) assert response.status_code == 200 # use XSRF_Cookie to send in csrf_token response = client.post( "/json_auth", json=dict(label="label"), headers={ "Content-Type": "application/json", "X-CSRF-Token": client.get_cookie("XSRF-Token").value, }, ) assert response.status_code == 200 assert response.json["label"] == "label" @pytest.mark.csrf(ignore_unauth=True, csrfprotect=True) @pytest.mark.settings(CSRF_COOKIE_NAME="XSRF-Token") def test_csrf_2fa_nounauth_cookie(app, client): # use CSRF cookie when ignoring unauth endpoints sms_sender = SmsSenderFactory.createSender("test") response = client.post( "/login", json=dict(email="gal@lp.com", password="password"), headers={"Content-Type": "application/json"}, ) code = sms_sender.messages[0].split()[-1] response = client.post( "/tf-validate", json=dict(code=code), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 response = client.post( "/json_auth", json=dict(label="label"), headers={ "Content-Type": "application/json", "X-CSRF-Token": client.get_cookie("XSRF-Token").value, }, ) assert response.status_code == 200 assert response.json["label"] == "label" def _tf_required(self, tf_setup_methods, tf_fresh): if self.email == "gal@lp.com": return False, tf_setup_methods return True, tf_setup_methods @pytest.mark.app_settings(TESTING_USER_INJECT=dict(check_tf_required=_tf_required)) @pytest.mark.settings(two_factor_required=True) def test_override_tf_required(app, client, get_message): data = dict(email="jill@lp.com", password="password") response = client.post("/login", json=data) assert response.status_code == 200 assert response.json["response"]["tf_required"] data = dict(email="gal@lp.com", password="password") response = client.post("/login", json=data) assert response.status_code == 200 assert not response.json["response"]["tf_required"] def _tf_required_admin(self, tf_setup_methods, tf_fresh): if self.has_role("admin"): return True, tf_setup_methods return False, tf_setup_methods @pytest.mark.app_settings( TESTING_USER_INJECT=dict(check_tf_required=_tf_required_admin) ) def test_override_tf_required_admin(app, client, get_message): # jill is an 'author' so shouldn't require TF data = dict(email="jill@lp.com", password="password") response = client.post("/login", json=data) assert response.status_code == 200 assert not response.json["response"]["tf_required"] client.post("/logout") # gal is an admin so should require TF data = dict(email="gal@lp.com", password="password") response = client.post("/login", json=data) assert response.status_code == 200 assert response.json["response"]["tf_required"] pallets-eco-flask-security-72bf929/tests/test_unified_signin.py000066400000000000000000002617621525721743500250310ustar00rootroot00000000000000""" test_unified_signin ~~~~~~~~~~~~~~~~~~~ Unified signin tests :copyright: (c) 2019-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ import base64 from contextlib import contextmanager from datetime import timedelta import markupsafe from passlib.totp import TOTP import re from urllib.parse import parse_qsl, urlsplit import pytest from flask import Flask from tests.conftest import v2_param from tests.test_utils import ( SmsBadSender, SmsTestSender, FakeSerializer, authenticate, capture_flashes, capture_reset_password_requests, check_location, check_signals, get_form_action, get_session, is_authenticated, logout, reset_fresh, reset_fresh_auth_token, setup_tf_sms, get_form_input, ) from tests.test_webauthn import HackWebauthnUtil, reg_2_keys from flask_security import ( SmsSenderFactory, UserMixin, uia_email_mapper, uia_phone_mapper, us_profile_changed, us_security_token_sent, user_authenticated, ) from flask_security.utils import get_identity_attributes pytestmark = pytest.mark.unified_signin() SmsSenderFactory.senders["test"] = SmsTestSender SmsSenderFactory.senders["bad"] = SmsBadSender UIA_EMAIL_PHONE = [ {"email": {"mapper": uia_email_mapper, "case_insensitive": True}}, {"us_phone_number": {"mapper": uia_phone_mapper}}, ] @contextmanager def capture_send_code_requests(): login_requests = [] def _on(app, **data): assert isinstance(app, Flask) assert all(v in data for v in ["user", "method", "token"]) assert isinstance(data["user"], UserMixin) login_requests.append(data) us_security_token_sent.connect(_on) try: yield login_requests finally: us_security_token_sent.disconnect(_on) def us_authenticate(client, identity="matt@lp.com"): with capture_send_code_requests() as requests: response = client.post( "/us-signin/send-code", json=dict(identity=identity, chosen_method="email"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 response = client.post( "/us-signin?include_auth_token", json=dict(identity=identity, passcode=requests[0]["token"]), headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 return response.json["response"]["user"]["authentication_token"] def us_tf_authenticate(app, client, json=False, validate=True, remember=False): """Login/Authenticate using two factor and unified signin This is the equivalent of utils:authenticate """ prev_sms = app.config["SECURITY_SMS_SERVICE"] app.config["SECURITY_SMS_SERVICE"] = "test" sms_sender = SmsSenderFactory.createSender("test") json_data = dict(identity="gal@lp.com", passcode="password", remember=remember) response = client.post( "/us-signin", json=json_data, headers={"Content-Type": "application/json"} ) assert b'"code": 200' in response.data app.config["SECURITY_SMS_SERVICE"] = prev_sms if validate: code = sms_sender.messages[0].split()[-1] if json: response = client.post( "/tf-validate", json=dict(code=code), headers={"Content-Type": "application/json"}, ) assert b'"code": 200' in response.data else: response = client.post( "/tf-validate", data=dict(code=code), follow_redirects=True ) assert response.status_code == 200 def set_phone(app, email="matt@lp.com", phone="650-273-3780"): # A quick way to 'setup' SMS with app.test_request_context("/"): user = app.security.datastore.find_user(email=email) totp_secret = app.security._totp_factory.generate_totp_secret() app.security.datastore.us_set(user, "sms", totp_secret, phone) app.security.datastore.commit() def set_email(app, email="matt@lp.com"): # A quick way to 'setup' email with app.test_request_context("/"): user = app.security.datastore.find_user(email=email) totp_secret = app.security._totp_factory.generate_totp_secret() app.security.datastore.us_set(user, "email", totp_secret) app.security.datastore.commit() def test_simple_signin(app, clients, get_message, outbox, signals): set_email(app) # Test missing choice data = dict(identity="matt@lp.com") response = clients.post("/us-signin/send-code", data=data, follow_redirects=True) assert get_message("US_METHOD_NOT_AVAILABLE") in response.data # Test login using invalid email data = dict(identity="nobody@lp.com", chosen_method="email") response = clients.post("/us-signin/send-code", data=data, follow_redirects=True) assert get_message("US_SPECIFY_IDENTITY") in response.data # test disabled account set_email(app, email="gal3@lp.com") with app.test_request_context("/"): user = app.security.datastore.find_user(email="gal3@lp.com") app.security.datastore.deactivate_user(user) app.security.datastore.commit() data = dict(identity="gal3@lp.com", chosen_method="email") response = clients.post("/us-signin/send-code", data=data, follow_redirects=True) assert b"Code has been sent" not in response.data assert get_message("DISABLED_ACCOUNT") in response.data with capture_send_code_requests() as requests: response = clients.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="email"), follow_redirects=True, ) assert response.status_code == 200 assert b"Sign In" in response.data assert len(requests) == 1 assert len(outbox) == 1 # try bad code response = clients.post( "/us-signin", data=dict(identity="matt@lp.com", passcode="blahblah"), follow_redirects=True, ) assert get_message("INVALID_PASSWORD_CODE") in response.data assert signals["user_failed_authn"][0]["request_endpoint"] == "security.us_signin" assert signals["user_failed_authn"][0]["user"].email == "matt@lp.com" assert signals["user_failed_authn"][0]["auth_type"] == "passcode" assert not signals["user_failed_authn"][0]["tfa"] # Correct code assert not clients.get_cookie("remember_token") assert not clients.get_cookie("session") response = clients.post( "/us-signin", data=dict(identity="matt@lp.com", passcode=requests[0]["token"]), follow_redirects=False, ) assert not clients.get_cookie("remember_token") assert signals["user_authenticated"][0]["authn_via"] == ["email"] assert is_authenticated(clients, get_message) logout(clients) assert not is_authenticated(clients, get_message) # login via SMS sms_sender = SmsSenderFactory.createSender("test") set_phone(app) response = clients.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="sms"), follow_redirects=True, ) assert response.status_code == 200 assert b"Sign In" in response.data code = sms_sender.messages[0].split()[-1].strip(".") response = clients.post( "/us-signin", data=dict(identity="matt@lp.com", passcode=code, remember=True), follow_redirects=True, ) assert response.status_code == 200 assert clients.get_cookie("remember_token") assert signals["user_authenticated"][1]["authn_via"] == ["sms"] assert is_authenticated(clients, get_message) logout(clients) assert not clients.get_cookie("remember_token") def test_simple_signin_json(app, client_nc, get_message, outbox, signals): set_email(app) headers = {"Accept": "application/json", "Content-Type": "application/json"} with capture_flashes() as flashes: response = client_nc.get("/us-signin", headers=headers) jresponse = response.json["response"] assert ( jresponse["available_methods"] == app.config["SECURITY_US_ENABLED_METHODS"] ) assert jresponse["identity_attributes"] == get_identity_attributes(app=app) assert set(jresponse["code_methods"]) == {"email", "sms"} with capture_send_code_requests() as requests: response = client_nc.post( "/us-signin/send-code", json=dict(identity="matt@lp.com", chosen_method="email"), headers=headers, follow_redirects=True, ) assert response.status_code == 200 assert "csrf_token" in response.json["response"] assert "user" not in response.json["response"] assert len(requests) == 1 assert len(outbox) == 1 # try bad code response = client_nc.post( "/us-signin", json=dict(identity="matt@lp.com", passcode="blahblah"), headers=headers, follow_redirects=True, ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_PASSWORD_CODE" ) assert ( signals["user_failed_authn"][0]["request_endpoint"] == "security.us_signin" ) assert signals["user_failed_authn"][0]["user"].email == "matt@lp.com" assert signals["user_failed_authn"][0]["auth_type"] == "passcode" assert not signals["user_failed_authn"][0]["tfa"] # Login successfully with code response = client_nc.post( "/us-signin?include_auth_token", json=dict(identity="matt@lp.com", passcode=requests[0]["token"]), headers=headers, follow_redirects=True, ) assert response.status_code == 200 assert "authentication_token" in response.json["response"]["user"] assert signals["user_authenticated"][0]["authn_via"] == ["email"] logout(client_nc) assert not is_authenticated(client_nc, get_message) # login via SMS sms_sender = SmsSenderFactory.createSender("test") set_phone(app) response = client_nc.post( "/us-signin/send-code", json=dict(identity="matt@lp.com", chosen_method="sms"), headers=headers, follow_redirects=True, ) assert response.status_code == 200 code = sms_sender.messages[0].split()[-1].strip(".") response = client_nc.post( "/us-signin?include_auth_token", json=dict(identity="matt@lp.com", passcode=code), headers=headers, follow_redirects=True, ) assert response.status_code == 200 assert "authentication_token" in response.json["response"]["user"] assert len(flashes) == 0 @pytest.mark.changeable() def test_signin_pwd_json(app, client, get_message): # Make sure us-signin accepts a normalized and original password. authenticate(client) headers = {"Accept": "application/json", "Content-Type": "application/json"} data = dict( password="password", new_password="new strong password\N{ROMAN NUMERAL ONE}", new_password_confirm="new strong password\N{ROMAN NUMERAL ONE}", ) response = client.post( "/change", json=data, headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 logout(client) response = client.post( "/us-signin", json=dict( identity="matt@lp.com", passcode="new strong password\N{ROMAN NUMERAL ONE}" ), headers=headers, follow_redirects=False, ) assert response.status_code == 200 logout(client) response = client.post( "/us-signin", json=dict( identity="matt@lp.com", passcode="new strong password\N{LATIN CAPITAL LETTER I}", ), headers=headers, follow_redirects=False, ) assert response.status_code == 200 @pytest.mark.registerable() @pytest.mark.settings( password_required=False, us_email_template="security/email/us_instructions_test", email_html=False, ) def test_us_passwordless(app, client, get_message, outbox): # Check passwordless. # Check contents of email template - this uses a test template # in order to check all context vars since the default template # doesn't have all of them. response = client.post( "/register", data=dict(email="nopasswd-dude@lp.com"), follow_redirects=True ) logout(client) with capture_send_code_requests() as requests: response = client.post( "/us-signin/send-code", data=dict(identity="nopasswd-dude@lp.com", chosen_method="email"), follow_redirects=True, ) # 2 emails - first from registration. assert len(outbox) == 2 matcher = re.findall(r"\w+:.*", outbox[1].body, re.IGNORECASE) # should be 6 - link, email, token, config item, username, within # and entire real template included assert matcher[1].split(":")[1] == "nopasswd-dude@lp.com" token = matcher[2].split(":")[1] assert token == requests[0]["token"] # deprecated assert token == requests[0]["login_token"] assert matcher[3].split(":")[1] == "True" # register_blueprint assert matcher[4].split(":")[1] == "nopasswd-dude@lp.com" assert matcher[5].split(":")[1] == "2 minutes" # regular text template is included in the test template assert "This code/link will expire in 2 minutes." in outbox[1].body # check link link = matcher[0].split(":", 1)[1] response = client.get(link, follow_redirects=True) assert get_message("PASSWORDLESS_LOGIN_SUCCESSFUL") in response.data # check us-setup has 'email' but not password response = client.get("/us-setup", json={}) assert response.json["response"]["active_methods"] == ["email"] @pytest.mark.registerable() @pytest.mark.confirmable() @pytest.mark.settings( password_required=False, us_email_template="security/email/us_instructions_test", email_html=False, ) def test_us_passwordless_confirm(app, client, get_message, outbox): # Check passwordless with confirmation required. response = client.post( "/register", data=dict(email="nopasswd-dude@lp.com"), follow_redirects=True ) # Try logging in - should get confirmation required. response = client.post( "/us-signin/send-code", data=dict(identity="nopasswd-dude@lp.com", chosen_method="email"), follow_redirects=True, ) assert get_message("CONFIRMATION_REQUIRED") in response.data # grab welcome email which has confirmation link (test version of welcome.txt) matcher = re.findall(r"\w+:.*", outbox[0].body, re.IGNORECASE) link = matcher[0].split(":", 1)[1] response = client.get(link, follow_redirects=True) assert get_message("EMAIL_CONFIRMED") in response.data logout(client) # should be able to authenticate now. response = client.post( "/us-signin/send-code", data=dict(identity="nopasswd-dude@lp.com", chosen_method="email"), follow_redirects=True, ) # 2 emails - first from registration. assert len(outbox) == 2 matcher = re.findall(r"\w+:.*", outbox[1].body, re.IGNORECASE) # authenticate with link link = matcher[0].split(":", 1)[1] response = client.get(link, follow_redirects=True) assert get_message("PASSWORDLESS_LOGIN_SUCCESSFUL") in response.data @pytest.mark.registerable() @pytest.mark.confirmable() @pytest.mark.settings( password_required=False, us_email_template="security/email/us_instructions_test", email_html=False, ) def test_us_passwordless_confirm_json(app, client, get_message, outbox): # Check passwordless with confirmation required. headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.post("/register", json=dict(email="nopasswd-dude@lp.com")) # Try logging in - should get confirmation required. response = client.post( "/us-signin/send-code", json=dict(identity="nopasswd-dude@lp.com", chosen_method="email"), ) assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "CONFIRMATION_REQUIRED" ) # grab welcome email which has confirmation link (test version of welcome.txt) matcher = re.findall(r"\w+:.*", outbox[0].body, re.IGNORECASE) link = matcher[0].split(":", 1)[1] response = client.get(link, headers=headers, follow_redirects=False) assert check_location(app, response.location, "/login") # should be able to authenticate now. response = client.post( "/us-signin/send-code", json=dict(identity="nopasswd-dude@lp.com", chosen_method="email"), ) # 2 emails - first from registration. assert len(outbox) == 2 matcher = re.findall(r"\w+:.*", outbox[1].body, re.IGNORECASE) # authenticate with link link = matcher[0].split(":", 1)[1] response = client.get(link, headers=headers, follow_redirects=True) assert get_message("PASSWORDLESS_LOGIN_SUCCESSFUL") in response.data def test_admin_setup_user_reset(app, client_nc, get_message): # Test that we can setup SMS using datastore admin method, and that # the datastore admin reset (reset_user_access) disables it. headers = {"Accept": "application/json", "Content-Type": "application/json"} sms_sender = SmsSenderFactory.createSender("test") set_phone(app) response = client_nc.post( "/us-signin/send-code", json=dict(identity="matt@lp.com", chosen_method="sms"), headers=headers, follow_redirects=True, ) assert response.status_code == 200 assert len(sms_sender.messages) == 1 code = sms_sender.messages[0].split()[-1].strip(".") response = client_nc.post( "/us-signin?include_auth_token", json=dict(identity="matt@lp.com", passcode=code), headers=headers, follow_redirects=True, ) assert response.status_code == 200 # logout, reset access logout(client_nc) with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") app.security.datastore.reset_user_access(user) app.security.datastore.commit() response = client_nc.post( "/us-signin/send-code", json=dict(identity="matt@lp.com", chosen_method="sms"), headers=headers, follow_redirects=True, ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "US_METHOD_NOT_AVAILABLE" ) # Nothing should have been sent. assert len(sms_sender.messages) == 1 def test_admin_setup_reset(app, client_nc, get_message): # Test that we can setup SMS using datastore admin method, and that # the datastore admin reset (us_reset) disables it. headers = {"Accept": "application/json", "Content-Type": "application/json"} sms_sender = SmsSenderFactory.createSender("test") set_phone(app) response = client_nc.post( "/us-signin/send-code", json=dict(identity="matt@lp.com", chosen_method="sms"), headers=headers, follow_redirects=True, ) assert response.status_code == 200 assert len(sms_sender.messages) == 1 with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") app.security.datastore.us_reset(user) app.security.datastore.commit() response = client_nc.post( "/us-signin/send-code", json=dict(identity="matt@lp.com", chosen_method="sms"), headers=headers, follow_redirects=True, ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "US_METHOD_NOT_AVAILABLE" ) # Nothing should have been sent. assert len(sms_sender.messages) == 1 @pytest.mark.settings(post_login_view="/post_login") def test_get_already_authenticated(app, client): response = authenticate(client, follow_redirects=True) assert b"Welcome matt@lp.com" in response.data # This should ignore next response = client.get("/us-signin?next=/page1", follow_redirects=True) assert b"Post Login" in response.data # should still get extra goodies headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.get("/us-signin", headers=headers) assert response.status_code == 200 jresponse = response.json["response"] assert all( a in jresponse for a in ["code_methods", "identity_attributes", "available_methods"] ) assert "authentication_token" not in jresponse["user"] assert all(a in jresponse["user"] for a in ["email", "last_update"]) @pytest.mark.settings(post_login_view="/post_login") def test_post_already_authenticated(client, get_message): response = authenticate(client, follow_redirects=True) assert b"Welcome matt@lp.com" in response.data data = dict(email="matt@lp.com", password="password") response = client.post("/us-signin", data=data, follow_redirects=True) assert b"Post Login" in response.data headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.post("/us-signin", json=data, headers=headers) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "ANONYMOUS_USER_REQUIRED" ) @pytest.mark.settings(us_email_subject="Code For You") def test_verify_link(app, client, get_message, outbox, signals): set_email(app) with capture_send_code_requests() as requests: response = client.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="email"), follow_redirects=True, ) assert response.status_code == 200 assert b"Sign In" in response.data assert outbox[0].recipients == ["matt@lp.com"] assert outbox[0].sender == "no-reply@localhost" assert outbox[0].subject == "Code For You" matcher = re.match( r".*(http://[^\s*]*).*", outbox[0].body, re.IGNORECASE | re.DOTALL ) magic_link = matcher.group(1) # Try with missing code response = client.get("us-verify-link?id=matt@lp.com", follow_redirects=False) assert "/us-signin" in response.location response = client.get("us-verify-link?id=matt@lp.com", follow_redirects=True) assert get_message("API_ERROR") in response.data # Try unknown user response = client.get("us-verify-link?id=123435&code=12345", follow_redirects=True) assert get_message("USER_DOES_NOT_EXIST") in response.data # Try bad code response = client.get( f"us-verify-link?id={requests[0]['user'].fs_uniquifier}&code=12345", follow_redirects=True, ) assert get_message("INVALID_CODE") in response.data assert ( signals["user_failed_authn"][0]["request_endpoint"] == "security.us_verify_link" ) assert signals["user_failed_authn"][0]["user"].email == "matt@lp.com" assert signals["user_failed_authn"][0]["auth_type"] == "passcode" assert not signals["user_failed_authn"][0]["tfa"] # Try actual link response = client.get(magic_link, follow_redirects=True) assert get_message("PASSWORDLESS_LOGIN_SUCCESSFUL") in response.data assert signals["user_authenticated"][0]["authn_via"] == ["email"] # verify logged in assert is_authenticated(client, get_message) check_signals(signals, {"user_failed_authn": 1, "us_security_token_sent": 1}) @pytest.mark.settings( redirect_host="localhost:8081", redirect_behavior="spa", login_error_view="/login-error", post_login_view="/post-login", ) def test_verify_link_spa(app, client, get_message, outbox, signals): # N.B. we use client here since this only works/ is supported if using # sessions. set_email(app) headers = {"Accept": "application/json", "Content-Type": "application/json"} with capture_send_code_requests() as requests: response = client.post( "/us-signin/send-code", json=dict(identity="matt@lp.com", chosen_method="email"), headers=headers, ) assert response.status_code == 200 matcher = re.match( r".*(http://[^\s*]*).*", outbox[0].body, re.IGNORECASE | re.DOTALL ) magic_link = matcher.group(1) # Try with no code response = client.get("us-verify-link?id=matt@lp.com", follow_redirects=False) assert response.status_code in [302, 303] split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/login-error" == split.path qparams = dict(parse_qsl(split.query)) assert get_message("API_ERROR") == qparams["error"].encode("utf-8") # Try unknown user response = client.get("us-verify-link?id=98765&code=12345", follow_redirects=False) assert response.status_code in [302, 303] split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/login-error" == split.path qparams = dict(parse_qsl(split.query)) assert get_message("USER_DOES_NOT_EXIST") == qparams["error"].encode("utf-8") # Try bad code response = client.get( f"us-verify-link?id={requests[0]['user'].fs_uniquifier}&code=12345", follow_redirects=False, ) assert response.status_code in [302, 303] split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/login-error" == split.path qparams = dict(parse_qsl(split.query)) assert get_message("INVALID_CODE") == qparams["error"].encode("utf-8") assert ( signals["user_failed_authn"][0]["request_endpoint"] == "security.us_verify_link" ) assert signals["user_failed_authn"][0]["user"].email == "matt@lp.com" assert signals["user_failed_authn"][0]["auth_type"] == "passcode" assert not signals["user_failed_authn"][0]["tfa"] # Try actual link response = client.get(magic_link, follow_redirects=False) assert response.status_code in [302, 303] split = urlsplit(response.headers["Location"]) assert "localhost:8081" == split.netloc assert "/post-login" == split.path qparams = dict(parse_qsl(split.query)) assert qparams["email"] == "matt@lp.com" check_signals(signals, {"user_failed_authn": 1, "us_security_token_sent": 1}) assert is_authenticated(client, get_message) def test_setup(app, clients, get_message, signals): tcl = clients set_email(app) us_authenticate(tcl) response = tcl.get("us-setup") # Email should be in delete options since we just set that up. assert all( i in response.data for i in [b"delete_method-0", b"chosen_method-0", b"chosen_method-1"] ) # test not supplying anything to do response = tcl.post("us-setup", data=dict(phone="6505551212")) assert get_message("API_ERROR") in response.data # test missing phone response = tcl.post("us-setup", data=dict(chosen_method="sms", phone="")) assert response.status_code == 200 assert get_message("PHONE_INVALID") in response.data # test invalid phone response = tcl.post( "us-setup", data=dict(chosen_method="sms", phone="NOT-A-NUMBER") ) assert response.status_code == 200 assert get_message("PHONE_INVALID") in response.data assert b"Enter code here to complete setup" not in response.data sms_sender = SmsSenderFactory.createSender("test") response = tcl.post( "us-setup", data=dict(chosen_method="sms", phone="650-555-1212") ) assert response.status_code == 200 assert b"Submit Code" in response.data assert b"Enter code here to complete setup" in response.data verify_url = get_form_action(response, 1) # Try invalid code response = tcl.post(verify_url, data=dict(passcode=12345), follow_redirects=True) assert get_message("INVALID_PASSWORD_CODE") in response.data # a bad code during setup shouldn't trigger the signal assert len(signals["user_failed_authn"]) == 0 code = sms_sender.messages[0].split()[-1].strip(".") response = tcl.post(verify_url, data=dict(passcode=code), follow_redirects=True) assert response.status_code == 200 assert get_message("US_SETUP_SUCCESSFUL") in response.data @pytest.mark.settings( us_email_template="security/email/us_instructions_test", email_html=False ) def test_setup_email(app, client, get_message, outbox): # setup with email - make sure magic link isn't sent and code is. # N.B. this is using the test us_instructions template set_email(app) us_authenticate(client) response = client.post("us-setup", data=dict(chosen_method="email")) assert response.status_code == 200 assert b"Enter code here to complete setup" in response.data verify_url = get_form_action(response, 1) # verify no magic link - us_authenticate received first email - we want the second matcher = re.findall(r"\w+:.*", outbox[1].body, re.IGNORECASE) # should be 4 - link, email, token, config item assert matcher[0].split(":")[1] == "None" assert matcher[1].split(":")[1] == "matt@lp.com" code = matcher[2].split(":")[1] response = client.post(verify_url, data=dict(passcode=code), follow_redirects=True) assert response.status_code == 200 assert get_message("US_SETUP_SUCCESSFUL") in response.data @pytest.mark.settings( us_enabled_methods=["email", "sms"], user_identity_attributes=UIA_EMAIL_PHONE, freshness=timedelta(hours=-1), ) def test_setup_json(app, client_nc, get_message): # This shows that by setting freshness to negative doesn't require session. @us_profile_changed.connect_via(app) def pc(sender, user, methods, delete, **kwargs): assert not delete assert methods == ["sms"] assert user.us_phone_number == "+16505551212" set_email(app) token = us_authenticate(client_nc) headers = { "Authentication-Token": token, "Accept": "application/json", "Content-Type": "application/json", } response = client_nc.get("/us-setup", headers=headers) assert response.status_code == 200 assert response.json["response"]["available_methods"] == ["email", "sms"] assert set(response.json["response"]["setup_methods"]) == {"email", "sms"} assert set(response.json["response"]["active_methods"]) == {"email", "password"} sms_sender = SmsSenderFactory.createSender("test") response = client_nc.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212"), headers=headers, ) assert response.status_code == 200 state = response.json["response"]["state"] assert state # send invalid code response = client_nc.post( "/us-setup/" + state, json=dict(passcode=12344), headers=headers ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_PASSWORD_CODE" ) code = sms_sender.messages[0].split()[-1].strip(".") response = client_nc.post( "/us-setup/" + state, json=dict(passcode=code), headers=headers ) assert response.status_code == 200 assert response.json["response"]["chosen_method"] == "sms" assert response.json["response"]["phone"] == "+16505551212" # Verify sms in list of 'active' methods response = client_nc.get("/us-setup", headers=headers) assert response.status_code == 200 assert set(response.json["response"]["active_methods"]) == { "email", "sms", "password", } # now login with phone - send in different format than we set up with. headers = {"Accept": "application/json", "Content-Type": "application/json"} sms_sender = SmsSenderFactory.createSender("test") response = client_nc.post( "/us-signin/send-code", json=dict(identity="6505551212", chosen_method="sms"), headers=headers, ) assert response.status_code == 200 code = sms_sender.messages[0].split()[-1].strip(".") response = client_nc.post( "/us-signin?include_auth_token", json=dict(identity="matt@lp.com", passcode=code), headers=headers, ) assert response.status_code == 200 assert "authentication_token" in response.json["response"]["user"] @pytest.mark.settings( us_enabled_methods=["email", "sms"], user_identity_attributes=UIA_EMAIL_PHONE, us_email_template="security/email/us_instructions_test", email_html=False, ) def test_setup_json_no_session(app, client_nc, get_message, outbox): # Test that with normal config freshness is required and we can use auth_token # for that set_email(app) us_authenticate(client_nc) token = reset_fresh_auth_token(app, app.config["SECURITY_FRESHNESS"]) headers = { "Authentication-Token": token, "Accept": "application/json", "Content-Type": "application/json", } response = client_nc.get("/us-setup", headers=headers) assert response.status_code == 401 assert response.json["response"]["reauth_required"] assert "WWW-Authenticate" not in response.headers # re-verify client_nc.post( "/us-verify/send-code", json=dict(identity="matt@lp.com", chosen_method="email"), headers=headers, ) matcher = re.match(r".*Token:(\d+).*", outbox[1].body, re.IGNORECASE | re.DOTALL) code = matcher.group(1) response = client_nc.post( "/us-verify?include_auth_token", json=dict(passcode=code), headers=headers ) assert response.status_code == 200 token = response.json["response"]["user"]["authentication_token"] headers["Authentication-Token"] = token # should work now response = client_nc.get("/us-setup", headers=headers) assert response.status_code == 200 @pytest.mark.settings(api_enabled_methods=["basic"]) def test_setup_basic(app, client, get_message): # If using Basic Auth - always fresh so should be able to setup (not sure the # use case but...) headers = { "Authorization": "Basic %s" % base64.b64encode(b"matt@lp.com:password").decode("utf-8") } response = client.get("/us-setup", headers=headers) assert response.status_code == 200 assert b"Setup Unified Sign In" in response.data def test_setup_bad_token(app, client, get_message): set_email(app) headers = {"Accept": "application/json", "Content-Type": "application/json"} us_authenticate(client) # bogus state response = client.post( "/us-setup/not a token", json=dict(passcode=12345), headers=headers ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) # same w/o json response = client.post( "/us-setup/not a token", data=dict(passcode=12345), follow_redirects=True ) assert get_message("API_ERROR") in response.data @pytest.mark.settings(us_setup_within=timedelta(seconds=47)) def test_setup_timeout(app, client, get_message): # Test setup timeout set_email(app) us_authenticate(client) headers = {"Accept": "application/json", "Content-Type": "application/json"} sms_sender = SmsSenderFactory.createSender("test") app.security.us_setup_serializer = FakeSerializer(47.0) response = client.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212"), headers=headers, ) assert response.status_code == 200 state = response.json["response"]["state"] code = sms_sender.messages[0].split()[-1].strip(".") response = client.post( "/us-setup/" + state, json=dict(passcode=code), headers=headers ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "US_SETUP_EXPIRED", within="47 seconds" ) @pytest.mark.settings( us_enabled_methods=["email", "sms"], user_identity_attributes=UIA_EMAIL_PHONE, ) def test_unique_phone(app, client, get_message): # Test that us_phone_number is properly validated to be unique set_email(app, email="matt@lp.com") us_authenticate(client, identity="matt@lp.com") sms_sender = SmsSenderFactory.createSender("test") response = client.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212"), ) assert response.status_code == 200 state = response.json["response"]["state"] assert state code = sms_sender.messages[0].split()[-1].strip(".") response = client.post("/us-setup/" + state, json=dict(passcode=code)) assert response.status_code == 200 logout(client) set_email(app, email="joe@lp.com") us_authenticate(client, identity="joe@lp.com") response = client.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212"), ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode() == get_message( "IDENTITY_ALREADY_ASSOCIATED", attr="us_phone_number", value="+16505551212" ) @pytest.mark.settings(freshness=timedelta(minutes=0)) def test_verify(app, clients, get_message, signals): client = clients # Test setup when re-authenticate required # With freshness set to 0 - the first call should require reauth (by # redirecting); but the second should work due to grace period. set_email(app) us_authenticate(client) response = client.get("us-setup", follow_redirects=False) verify_url = response.location assert check_location(app, response.location, "/us-verify?next=/us-setup") logout(client) us_authenticate(client) response = client.get("us-setup", follow_redirects=True) form_response = response.data.decode("utf-8") assert "Reauthenticate" in form_response send_code_url = get_form_action(response, 1) # Send unknown method response = client.post( send_code_url, data=dict(identity="matt@lp.com", chosen_method="sms2"), follow_redirects=True, ) assert response.status_code == 200 assert b"Not a valid choice" in response.data # Verify using SMS sms_sender = SmsSenderFactory.createSender("test") set_phone(app) response = client.post( send_code_url, data=dict(identity="matt@lp.com", chosen_method="sms"), follow_redirects=True, ) assert response.status_code == 200 assert b"Code has been sent" in response.data code = sms_sender.messages[0].split()[-1].strip(".") response = client.post(verify_url, data=dict(passcode=code), follow_redirects=False) assert check_location(app, response.location, "/us-setup") check_signals(signals, {"user_failed_authn": 0, "us_security_token_sent": 3}) def test_verify_json(app, client, get_message, signals): # Test setup when re-authenticate required # N.B. with freshness=0 we never set a grace period and should never be able to # get to /us-setup set_email(app) us_authenticate(client) headers = {"Accept": "application/json", "Content-Type": "application/json"} app.config["SECURITY_FRESHNESS"] = timedelta(minutes=0) response = client.get("us-setup", headers=headers) assert response.status_code == 401 assert response.json["response"]["reauth_required"] # figure out which methods are usable response = client.get("us-verify", headers=headers) assert response.json["response"]["available_methods"] == [ "password", "email", "authenticator", "sms", ] # code_methods should just contain active/setup methods. assert set(response.json["response"]["code_methods"]) == { "email", } assert not response.json["response"]["oauth_enabled"] assert not response.json["response"]["oauth_providers"] response = client.post( "us-verify/send-code", json=dict(chosen_method="orb"), headers=headers, ) assert response.status_code == 400 # Verify using SMS sms_sender = SmsSenderFactory.createSender("test") set_phone(app) response = client.post( "us-verify/send-code", json=dict(chosen_method="sms"), headers=headers, ) assert response.status_code == 200 # Try bad code response = client.post("us-verify", json=dict(passcode=42), headers=headers) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_PASSWORD_CODE" ) assert response.json["response"]["field_errors"]["passcode"][0].encode( "utf-8" ) == get_message("INVALID_PASSWORD_CODE") assert signals["user_failed_authn"][0]["request_endpoint"] == "security.us_verify" assert signals["user_failed_authn"][0]["user"].email == "matt@lp.com" assert signals["user_failed_authn"][0]["auth_type"] == "passcode" assert not signals["user_failed_authn"][0]["tfa"] response = client.post("us-verify", json=dict(passcode=None), headers=headers) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_PASSWORD_CODE" ) code = sms_sender.messages[0].split()[-1].strip(".") response = client.post("us-verify", json=dict(passcode=code), headers=headers) assert response.status_code == 200 app.config["SECURITY_FRESHNESS"] = timedelta(minutes=60) response = client.get("us-setup", headers=headers) assert response.status_code == 200 assert len(signals["user_failed_authn"]) == 1 check_signals(signals, {"user_failed_authn": 1, "us_security_token_sent": 2}) @pytest.mark.settings(freshness=timedelta(minutes=-1)) def test_setup_nofresh(app, client, get_message): set_email(app) us_authenticate(client) response = client.get("us-setup", follow_redirects=False) assert response.status_code == 200 response = client.get("us-verify") assert response.status_code == 404 @pytest.mark.settings(us_enabled_methods=["sms"]) def test_invalid_method(app, client, get_message): headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.get("/us-signin", headers=headers) assert response.json["response"]["available_methods"] == ["sms"] # verify json error response = client.post( "/us-signin/send-code", json=dict(identity="matt@lp.com", chosen_method="email"), headers=headers, follow_redirects=True, ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "US_METHOD_NOT_AVAILABLE" ) # verify form error response = client.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="email"), follow_redirects=True, ) assert response.status_code == 200 assert get_message("US_METHOD_NOT_AVAILABLE") in response.data @pytest.mark.settings(us_enabled_methods=["sms", "email"]) def test_invalid_method_setup(app, client, get_message): set_email(app) us_authenticate(client) headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.get("/us-setup", headers=headers) assert response.json["response"]["available_methods"] == ["sms", "email"] # verify json error response = client.post( "/us-setup", json=dict(email="matt@lp.com", chosen_method="authenticator"), headers=headers, follow_redirects=True, ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "US_METHOD_NOT_AVAILABLE" ) response = client.post( "/us-setup", data=dict(email="matt@lp.com", chosen_method="authenticators"), follow_redirects=True, ) assert get_message("US_METHOD_NOT_AVAILABLE") in response.data def test_setup_new_totp(app, client, get_message): # us-setup should generate a new totp secret for each setup # Verify existing codes no longer work set_email(app) us_authenticate(client) headers = {"Accept": "application/json", "Content-Type": "application/json"} # Start by generating a good code - this will generate a new totp sms_sender = SmsSenderFactory.createSender("test") response = client.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212"), headers=headers, ) assert response.status_code == 200 assert "authr_key" not in response.json["response"] code = sms_sender.messages[0].split()[-1].strip(".") # Now start setup again - it should generate a new totp - so the previous 'code' # should no longer work sms_sender2 = SmsSenderFactory.createSender("test") response = client.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212"), headers=headers, ) assert response.status_code == 200 state = response.json["response"]["state"] # Use old code response = client.post( "/us-setup/" + state, json=dict(passcode=code), headers=headers ) assert response.status_code == 400 # Use new code code = sms_sender2.messages[0].split()[-1].strip(".") response = client.post( "/us-setup/" + state, json=dict(passcode=code), headers=headers ) assert response.status_code == 200 assert response.json["response"]["chosen_method"] == "sms" assert response.json["response"]["phone"] == "+16505551212" def test_qrcode(app, client, get_message): # Test forms based authenticator setup - can't really parse QRcode - but can use # the key sent as part of the response. set_email(app, email="gal@lp.com") us_authenticate(client, identity="gal@lp.com") response = client.post("us-setup", data=dict(chosen_method="authenticator")) assert response.status_code == 200 # verify png QRcode is present assert b"data:image/svg+xml;base64," in response.data # parse out key rd = response.data.decode("utf-8") matcher = re.match(r".*((?:\S{4}-){7}\S{4}).*", rd, re.DOTALL) totp_secret = matcher.group(1) # Generate token from passed totp_secret and confirm setup totp = TOTP(totp_secret) code = totp.generate().token # get verify link e.g. /us-setup/{state} verify_url = get_form_action(response, 1) response = client.post(verify_url, data=dict(passcode=code), follow_redirects=True) assert response.status_code == 200 assert get_message("US_SETUP_SUCCESSFUL") in response.data def test_next(app, client, get_message): set_email(app) with capture_send_code_requests() as requests: response = client.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="email"), follow_redirects=True, ) assert response.status_code == 200 response = client.post( "/us-signin?next=/post_login", data=dict(identity="matt@lp.com", passcode=requests[0]["token"]), follow_redirects=False, ) assert "/post_login" in response.location logout(client) # Test form.next response = client.post( "/us-signin", data=dict( identity="matt@lp.com", passcode=requests[0]["token"], next="/post_login" ), follow_redirects=False, ) assert "/post_login" in response.location def test_us_verify_form_next(app, client, get_message): # /us-verify should honor form.next and validate it (issue #1108) set_email(app) us_authenticate(client) response = client.get("/us-verify?next=/profile") assert b'name="next"' in response.data assert b'value="/profile"' in response.data response = client.post( "/us-verify", data=dict(passcode="password", next="/profile"), follow_redirects=False, ) assert check_location(app, response.location, "/profile") response = client.post( "/us-verify", data=dict(passcode="password", next="http://evil.example/phish"), follow_redirects=False, ) assert response.status_code == 200 assert get_message("INVALID_REDIRECT") in response.data @pytest.mark.registerable() @pytest.mark.confirmable() @pytest.mark.settings(requires_confirmation_error_view="/confirm") def test_requires_confirmation_error_redirect(app, client): data = dict( email="jyl@lp.com", password="password", password_confirm="password", next="" ) response = client.post("/register", data=data, follow_redirects=True) set_email(app, email="jyl@lp.com") response = client.post( "/us-signin/send-code", data=dict(identity="jyl@lp.com", chosen_method="email"), follow_redirects=False, ) assert response.location == "/confirm?email=jyl%40lp.com" response = client.post( "/us-signin", data=dict(identity="jyl@lp.com", passcode="password"), follow_redirects=False, ) assert response.location == "/confirm?email=jyl%40lp.com" @pytest.mark.registerable() @pytest.mark.confirmable() def test_confirmable(app, client, get_message, signals): # Verify can't log in if need confirmation. data = dict( email="dude@lp.com", password="password", password_confirm="password", next="" ) response = client.post("/register", data=data, follow_redirects=True) assert response.status_code == 200 set_email(app, email="dude@lp.com") response = client.post( "/us-signin/send-code", data=dict(identity="dude@lp.com", chosen_method="email"), follow_redirects=True, ) assert response.status_code == 200 assert get_message("CONFIRMATION_REQUIRED") in response.data check_signals(signals, "user_registered") # Verify not authenticated assert not is_authenticated(client, get_message) @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.registerable() @pytest.mark.recoverable() @pytest.mark.settings(password_required=False) def test_can_add_password(app, client, get_message): # Test that if register w/o a password, can use 'recover password' to assign one data = dict(email="trp@lp.com", password="", password_confirm="") response = client.post("/register", data=data, follow_redirects=True) assert b"Welcome trp@lp.com" in response.data logout(client) with capture_reset_password_requests() as requests: client.post("/reset", data=dict(email="trp@lp.com"), follow_redirects=True) token = requests[0]["token"] response = client.post( "/reset/" + token, data={"password": "awesome sunset", "password_confirm": "awesome sunset"}, follow_redirects=True, ) assert get_message("PASSWORD_RESET_NO_LOGIN") in response.data # authenticate with new password using standard/old login endpoint. response = authenticate( client, "trp@lp.com", "awesome sunset", follow_redirects=True ) assert b"Welcome trp@lp.com" in response.data logout(client) # authenticate with password and us-signin endpoint response = client.post( "/us-signin", data=dict(identity="trp@lp.com", passcode="awesome sunset"), follow_redirects=True, ) assert b"Welcome trp@lp.com" in response.data @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.registerable() @pytest.mark.changeable() @pytest.mark.settings( password_required=False, us_email_template="security/email/us_instructions_test", email_html=False, ) def test_change_empty_password(app, client, outbox): # test that if register w/o a password - can 'change' it. headers = {"Accept": "application/json", "Content-Type": "application/json"} data = dict(email="trp@lp.com", password="", password_confirm="") response = client.post("/register", data=data, follow_redirects=True) # should have been logged in since no confirmation # make sure requires a fresh authentication reset_fresh(client, app.config["SECURITY_FRESHNESS"]) data = dict( password="", new_password="awesome sunset", new_password_confirm="awesome sunset", ) response = client.post("/change", json=data) assert response.status_code == 401 assert response.json["response"]["reauth_required"] client.post( "/us-verify/send-code", json=dict(identity="trp@lp.com", chosen_method="email"), ) matcher = re.match(r".*Token:(\d+).*", outbox[1].body, re.IGNORECASE | re.DOTALL) code = matcher.group(1) response = client.post("/us-verify", json=dict(passcode=code)) assert response.status_code == 200 response = client.get("/change", headers=headers) assert not response.json["response"]["active_password"] response = client.get("/change") assert b"You do not" in response.data # now should be able to change response = client.post("/change", json=data) assert response.status_code == 200 logout(client) response = client.post( "/login", json=dict(email="trp@lp.com", password="awesome sunset") ) assert response.status_code == 200 @pytest.mark.parametrize("app", v2_param, indirect=True) @pytest.mark.registerable() @pytest.mark.settings(password_required=False) def test_empty_password(app, client, get_message): # test that if no password - can't log in with empty password data = dict(email="trp@lp.com", password="", password_confirm="") response = client.post("/register", data=data, follow_redirects=True) logout(client) response = client.post("/us-signin", json=dict(identity="trp@lp.com", passcode="")) assert response.status_code == 400 assert response.json["response"]["field_errors"]["passcode"][0].encode( "utf-8" ) == get_message("INVALID_PASSWORD_CODE") assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "INVALID_PASSWORD_CODE" ) @pytest.mark.settings( us_enabled_methods=["password"], user_identity_attributes=[ {"email": {"mapper": uia_email_mapper}}, {"username": {"mapper": lambda x: x}}, ], ) def test_regular_login(app, client, get_message): # If "password" in methods - then should be able to login with good-ol # login/password. # By having username - this also checks that we properly stop at the first # mapping. auths = [] @user_authenticated.connect_via(app) def authned(myapp, user, **extra_args): auths.append((user.email, extra_args["authn_via"])) response = client.post( "/us-signin", data=dict(identity="matt@lp.com", passcode="password", remember=True), follow_redirects=True, ) assert response.status_code == 200 assert client.get_cookie("remember_token") assert "password" in auths[0][1] assert is_authenticated(client, get_message) @pytest.mark.settings( us_enabled_methods=["sms"], user_identity_attributes=UIA_EMAIL_PHONE ) def test_regular_login_disallowed(app, client, get_message): # If "password" not in methods - then should not be able to use password response = client.post( "/us-signin", data=dict(identity="matt@lp.com", passcode="password", remember=True), follow_redirects=True, ) assert get_message("INVALID_PASSWORD_CODE") in response.data @pytest.mark.two_factor() @pytest.mark.settings(two_factor_required=True) def test_tf(app, client, get_message): # Test basic two-factor - default for signing in with password. response = client.post( "/us-signin", data=dict(identity="matt@lp.com", passcode="password", remember=True), follow_redirects=True, ) assert response.status_code == 200 message = b"Two-Factor authentication adds an extra layer of security" assert message in response.data assert b"Set up using SMS" in response.data sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") response = client.post("/tf-setup", data=data, follow_redirects=True) assert b"Enter code to complete setup" in response.data code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", data=dict(code=code), follow_redirects=True) assert get_message("TWO_FACTOR_LOGIN_SUCCESSFUL") in response.data @pytest.mark.two_factor() @pytest.mark.settings(two_factor_required=True) def test_tf_link(app, client, get_message, outbox): # Verify two-factor required when using magic link set_email(app) response = client.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="email"), follow_redirects=True, ) assert response.status_code == 200 assert b"Sign In" in response.data matcher = re.match( r".*(http://[^\s*]*).*", outbox[0].body, re.IGNORECASE | re.DOTALL ) magic_link = matcher.group(1) response = client.get(magic_link, follow_redirects=True) assert get_message("PASSWORDLESS_LOGIN_SUCCESSFUL") not in response.data message = b"Two-Factor authentication adds an extra layer of security" assert message in response.data @pytest.mark.two_factor() @pytest.mark.settings(two_factor_required=True, us_mfa_required=["password"]) def test_tf_link_not_email(app, client, get_message, outbox): # Verify two-factor not required when using magic link and us_mfa_required set auths = [] @user_authenticated.connect_via(app) def authned(myapp, user, **extra_args): auths.append((user.email, extra_args["authn_via"])) set_email(app) response = client.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="email"), follow_redirects=True, ) assert response.status_code == 200 assert b"Sign In" in response.data matcher = re.match( r".*(http://[^\s*]*).*", outbox[0].body, re.IGNORECASE | re.DOTALL ) magic_link = matcher.group(1) client.get(magic_link, follow_redirects=True) assert "email" in auths[0][1] assert is_authenticated(client, get_message) @pytest.mark.two_factor() @pytest.mark.settings( two_factor_required=True, redirect_host="localhost:8081", redirect_behavior="spa", login_error_view="/login-error", ) def test_tf_link_spa(app, client, get_message, outbox): # Verify two-factor required when using magic link and SPA # This currently isn't supported and should redirect to an error. set_email(app) response = client.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="email"), follow_redirects=True, ) assert response.status_code == 200 assert b"Sign In" in response.data matcher = re.match( r".*(http://[^\s*]*).*", outbox[0].body, re.IGNORECASE | re.DOTALL ) magic_link = matcher.group(1) response = client.get(magic_link, follow_redirects=False) split = urlsplit(response.location) assert "localhost:8081" == split.netloc assert "/login-error" == split.path qparams = dict(parse_qsl(split.query)) assert qparams["tf_required"] == "1" assert qparams["email"] == "matt@lp.com" @pytest.mark.two_factor() @pytest.mark.settings( two_factor_required=True, user_identity_attributes=UIA_EMAIL_PHONE ) def test_tf_not(app, client, get_message): # Test basic two-factor - when first factor doesn't require second (e.g. SMS) # 1. sign in and setup TFA client.post( "/us-signin", data=dict(identity="matt@lp.com", passcode="password", remember=True), follow_redirects=True, ) sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661177") client.post("/tf-setup", data=data, follow_redirects=True) code = sms_sender.messages[0].split()[-1] client.post("/tf-validate", data=dict(code=code), follow_redirects=True) # 2. setup unified sign in with SMS response = client.post( "us-setup", data=dict(chosen_method="sms", phone="650-555-1212") ) verify_url = get_form_action(response, 1) code = sms_sender.messages[0].split()[-1].strip(".") response = client.post(verify_url, data=dict(passcode=code), follow_redirects=True) assert response.status_code == 200 assert get_message("US_SETUP_SUCCESSFUL") in response.data # 3. logout logout(client) # 4. sign in with SMS - should not require TFA client.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="sms"), follow_redirects=True, ) code = sms_sender.messages[0].split()[-1].strip(".") response = client.post( "/us-signin", data=dict(identity="6505551212", passcode=code), follow_redirects=True, ) assert response.status_code == 200 # assert "sms" in auths[1][1] # Verify authenticated assert is_authenticated(client, get_message) @pytest.mark.settings(sms_service="bad") def test_bad_sender(app, client, get_message): # If SMS sender fails - make sure propagated # Test form, json, x signin, setup headers = {"Accept": "application/json", "Content-Type": "application/json"} set_phone(app) data = dict(identity="matt@lp.com", chosen_method="sms") response = client.post("/us-signin/send-code", data=data, follow_redirects=True) assert get_message("FAILED_TO_SEND_CODE") in response.data response = client.post("us-signin/send-code", json=data, headers=headers) assert response.status_code == 500 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "FAILED_TO_SEND_CODE" ) # Now test setup set_email(app) us_authenticate(client) data = dict(chosen_method="sms", phone="650-555-1212") response = client.post("us-setup", data=data) assert get_message("FAILED_TO_SEND_CODE") in response.data response = client.post("us-setup", json=data, headers=headers) assert response.status_code == 500 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "FAILED_TO_SEND_CODE" ) # Test us-verify data = dict(chosen_method="sms") response = client.post("/us-verify/send-code", data=data) assert get_message("FAILED_TO_SEND_CODE") in response.data response = client.post("us-verify/send-code", json=data) assert response.status_code == 500 assert response.json["response"]["field_errors"]["chosen_method"][0].encode( "utf-8" ) == get_message("FAILED_TO_SEND_CODE") def _send_code(self, method, **kwargs): assert method == "sms" return "NO SMS AVAILABLE" @pytest.mark.app_settings(TESTING_USER_INJECT=dict(us_send_security_token=_send_code)) @pytest.mark.registerable() def test_replace_send_code(app, client, get_message): set_phone(app, "gal@lp.com") data = dict(identity="gal@lp.com", chosen_method="sms") response = client.post("/us-signin/send-code", data=data, follow_redirects=True) assert b"NO SMS AVAILABLE" in response.data @pytest.mark.settings(us_enabled_methods=["password"]) def test_only_passwd(app, client, get_message): authenticate(client) response = client.get("us-setup") assert b"No method" in response.data headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.get("us-setup", headers=headers) assert response.json["response"]["available_methods"] == ["password"] assert not response.json["response"]["setup_methods"] @pytest.mark.settings(us_enabled_methods=["password", "authenticator"]) def test_passwd_and_authenticator(app, client, get_message): authenticate(client) response = client.get("us-setup") assert b"authenticator app" in response.data # Check that we get QRcode URL and no 'code sent' response = client.post("us-setup", data=dict(chosen_method="authenticator")) assert response.status_code == 200 assert b"Code has been sent" not in response.data assert b"Open an authenticator app" in response.data # verify png QRcode is present assert b"data:image/svg+xml;base64," in response.data headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.get("us-setup", headers=headers) assert response.json["response"]["available_methods"] == [ "password", "authenticator", ] assert response.json["response"]["setup_methods"] == ["authenticator"] def test_totp_generation(app, client, get_message): # Test that we generate a new totp on each setup of a different method # and that on failure to validate, the secret is NOT changed in the DB # and on successful validation, it is. headers = {"Accept": "application/json", "Content-Type": "application/json"} authenticate(client, email="dave@lp.com") with app.app_context(): user = app.security.datastore.find_user(email="dave@lp.com") ts = app.security.datastore.us_get_totp_secrets(user) assert "authenticator" not in ts response = client.post( "us-setup", json=dict(chosen_method="authenticator"), headers=headers ) assert response.status_code == 200 jr = response.json["response"] assert jr["authr_issuer"] == "tests" assert jr["authr_username"] == "dave@lp.com" assert "authr_key" in response.json["response"] assert len(jr["authr_b32key"]) % 8 == 0 and re.match( r"^[A-Z2-7]+=*$", jr["authr_b32key"] ) assert ( jr["authr_uri"] == f"otpauth://totp/tests:dave@lp.com?secret={jr['authr_b32key']}&issuer=tests" ) state = response.json["response"]["state"] # Generate token from passed totp_secret and confirm setup totp_key = response.json["response"]["authr_key"] totp = TOTP(totp_key) code = totp.generate().token response = client.post( "/us-setup/" + state, json=dict(passcode=code), headers=headers ) assert response.status_code == 200 assert response.json["response"]["chosen_method"] == "authenticator" # success - totp_secret in DB should have been saved with app.app_context(): user = app.security.datastore.find_user(email="dave@lp.com") ts = app.security.datastore.us_get_totp_secrets(user) assert ( app.security._totp_factory.get_totp_pretty_key(ts["authenticator"]) == totp_key ) # Now setup SMS and verify that authenticator totp hasn't changed sms_sender = SmsSenderFactory.createSender("test") response = client.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212"), headers=headers, ) assert response.status_code == 200 code = sms_sender.messages[0].split()[-1].strip(".") state = response.json["response"]["state"] response = client.post( "/us-setup/" + state, json=dict(passcode=code), headers=headers ) assert response.status_code == 200 assert response.json["response"]["chosen_method"] == "sms" assert response.json["response"]["phone"] == "+16505551212" # make sure authenticator totp hasn't changed. with app.app_context(): user = app.security.datastore.find_user(email="dave@lp.com") ts = app.security.datastore.us_get_totp_secrets(user) assert ( app.security._totp_factory.get_totp_pretty_key(ts["authenticator"]) == totp_key ) # Ok - setup again - but send invalid code - check totp in DB didn't change. response = client.post( "us-setup", json=dict(chosen_method="authenticator"), headers=headers ) assert response.status_code == 200 state = response.json["response"]["state"] totp_key = response.json["response"]["authr_key"] # validate with wrong code response = client.post( "/us-setup/" + state, json=dict(passcode=123345), headers=headers ) assert response.status_code == 400 # Make sure totp_secret in DB didn't change with app.app_context(): user = app.security.datastore.find_user(email="dave@lp.com") ts = app.security.datastore.us_get_totp_secrets(user) assert ( app.security._totp_factory.get_totp_pretty_key(ts["authenticator"]) != totp_key ) @pytest.mark.two_factor() @pytest.mark.settings( two_factor_required=True, user_identity_attributes=UIA_EMAIL_PHONE, two_factor_always_validate=False, ) def test_us_tf_validity(app, client, get_message): us_tf_authenticate(app, client, remember=True) assert client.get_cookie(app.config["SECURITY_TWO_FACTOR_VALIDITY_COOKIE_NAME"]) logout(client) # logout does NOT remove this cookie assert client.get_cookie(app.config["SECURITY_TWO_FACTOR_VALIDITY_COOKIE_NAME"]) # This time shouldn't require code data = dict(identity="gal@lp.com", passcode="password") response = client.post("/us-signin", json=data) assert response.json["meta"]["code"] == 200 assert is_authenticated(client, get_message) logout(client) data = dict(identity="gal2@lp.com", passcode="password") response = client.post("/us-signin", data=data, follow_redirects=True) assert b"Please enter your authentication code" in response.data response = client.post( "/us-signin", json=data, follow_redirects=True, headers={"Content-Type": "application/json"}, ) assert response.status_code == 200 assert response.json["response"]["tf_primary_method"] == "authenticator" assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "ready" @pytest.mark.webauthn(webauthn_util_cls=HackWebauthnUtil) def test_us_verify_wan(app, client, get_message): # test get correct options when requiring a reauthentication and have wan keys # setup. headers = {"Accept": "application/json", "Content-Type": "application/json"} reg_2_keys(client) reset_fresh(client, app.config["SECURITY_FRESHNESS"]) response = client.get("us-setup", headers=headers) assert response.status_code == 401 assert response.json["response"]["reauth_required"] assert response.json["response"]["has_webauthn_verify_credential"] # the us-verify form should have the webauthn verify form attached response = client.get("us-verify") assert b'action="/wan-verify"' in response.data app.config["SECURITY_WAN_ALLOW_AS_VERIFY"] = None response = client.get("us-setup", headers=headers) assert response.status_code == 401 assert response.json["response"]["reauth_required"] assert not response.json["response"]["has_webauthn_verify_credential"] # the us-verify form should NOT have the webauthn verify form attached response = client.get("us-verify") assert b'action="/wan-verify"' not in response.data def test_setup_delete(app, client, get_message): set_email(app) us_authenticate(client) response = client.get("us-setup") # Email should be in delete options since we just set that up. assert all( i in response.data for i in [b"delete_method-0", b"chosen_method-0", b"chosen_method-1"] ) response = client.post("us-setup", data=dict(delete_method="email")) response = client.get("us-setup") # All should be in possible setups. assert all( i in response.data for i in [b"chosen_method-0", b"chosen_method-1", b"chosen_method-2"] ) response = client.post("us-setup", json=dict(delete_method="email")) assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "US_METHOD_NOT_AVAILABLE" ) def test_setup_delete_json(app, client, get_message): recorded = [] @us_profile_changed.connect_via(app) def pc(sender, user, methods, delete, **kwargs): if "sms" in methods: if delete: assert not user.us_phone_number recorded.append("delete") else: assert user.us_phone_number == "+16505551212" recorded.append("setup") headers = {"Accept": "application/json", "Content-Type": "application/json"} set_email(app) us_authenticate(client) response = client.get("us-setup", headers=headers) # Email should be in delete options since we just set that up. assert "email" in response.json["response"]["active_methods"] response = client.post("us-setup", json=dict(delete_method="email")) assert response.status_code == 200 response = client.get("us-setup", headers=headers) assert response.json["response"]["active_methods"] == ["password"] response = client.post("us-setup", json=dict(delete_method="email")) assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "US_METHOD_NOT_AVAILABLE" ) # setup and delete SMS sms_sender = SmsSenderFactory.createSender("test") response = client.post( "us-setup", json=dict(chosen_method="sms", phone="650-555-1212"), headers=headers, ) assert response.status_code == 200 state = response.json["response"]["state"] assert state code = sms_sender.messages[0].split()[-1].strip(".") response = client.post( "/us-setup/" + state, json=dict(passcode=code), headers=headers ) assert response.status_code == 200 assert response.json["response"]["chosen_method"] == "sms" assert response.json["response"]["phone"] == "+16505551212" # verify SMS in active methods response = client.get("/us-setup", headers=headers) assert response.status_code == 200 assert set(response.json["response"]["active_methods"]) == {"sms", "password"} # delete SMS response = client.post("/us-setup", json=(dict(delete_method="sms"))) assert response.status_code == 200 response = client.get("/us-setup", headers=headers) assert response.json["response"]["active_methods"] == ["password"] assert recorded[0] == "setup" assert recorded[1] == "delete" def test_setup_delete_multi_json(app, client, get_message): recorded = [] @us_profile_changed.connect_via(app) def pc(sender, user, methods, delete, **kwargs): recorded.append((delete, methods)) headers = {"Accept": "application/json", "Content-Type": "application/json"} set_email(app) set_phone(app) us_authenticate(client) response = client.get("us-setup", headers=headers) # Email and sms should be in delete options since we just set that up. assert set(response.json["response"]["active_methods"]) == { "sms", "password", "email", } response = client.post("us-setup", json=dict(delete_method=["email", "sms"])) assert response.status_code == 200 response = client.get("us-setup", headers=headers) assert response.json["response"]["active_methods"] == ["password"] assert len(recorded) == 1 assert set(recorded[0][1]) == {"sms", "email"} @pytest.mark.settings(return_generic_responses=True) def test_generic_response(app, client, get_message): # test not-setup choice data = dict(identity="matt@lp.com", chosen_method="email") response = client.post("/us-signin/send-code", data=data, follow_redirects=True) assert get_message("GENERIC_US_SIGNIN") in response.data # for JSON still return 200 as if everything is fine and a code was sent. response = client.post("/us-signin/send-code", json=data) assert response.status_code == 200 assert not any(e in response.json["response"].keys() for e in ["error", "errors"]) # Correct method should return same thing set_phone(app) data = dict(identity="matt@lp.com", chosen_method="sms") response = client.post("/us-signin/send-code", data=data, follow_redirects=True) assert get_message("GENERIC_US_SIGNIN") in response.data # for JSON still return 200 as if everything is fine and a code was sent. response = client.post("/us-signin/send-code", json=data) assert response.status_code == 200 assert not any( e in response.json["response"].keys() for e in ["field_errors", "errors"] ) # Unknown identity should return same thing data = dict(identity="matt2@lp.com", chosen_method="email") response = client.post("/us-signin/send-code", data=data, follow_redirects=True) assert get_message("GENERIC_US_SIGNIN") in response.data # for JSON still return 200 as if everything is fine and a code was sent. response = client.post("/us-signin/send-code", json=data) assert response.status_code == 200 assert not any( e in response.json["response"].keys() for e in ["field_errors", "errors"] ) # # Now test us-signin itself # data = dict(identity="matt@lp.com", code="12345") response = client.post("/us-signin", data=data) assert get_message("GENERIC_AUTHN_FAILED") in response.data data = dict(identity="matt@lp.com", code="12345") response = client.post("/us-signin", json=data) assert response.status_code == 400 assert list(response.json["response"]["field_errors"].keys()) == [""] assert len(response.json["response"]["field_errors"][""]) == 1 assert response.json["response"]["field_errors"][""][0].encode( "utf-8" ) == get_message("GENERIC_AUTHN_FAILED") assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "GENERIC_AUTHN_FAILED" ) # same with unknown user data = dict(identity="matt2@lp.com", code="12345") response = client.post("/us-signin", data=data) assert get_message("GENERIC_AUTHN_FAILED") in response.data data = dict(identity="matt2@lp.com", code="12345") response = client.post("/us-signin", json=data) assert response.status_code == 400 assert list(response.json["response"]["field_errors"].keys()) == [""] assert len(response.json["response"]["field_errors"][""]) == 1 assert response.json["response"]["field_errors"][""][0].encode( "utf-8" ) == get_message("GENERIC_AUTHN_FAILED") assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "GENERIC_AUTHN_FAILED" ) # # Test /us-verify-link # set_email(app) with capture_send_code_requests() as requests: response = client.post( "/us-signin/send-code", data=dict(identity="matt@lp.com", chosen_method="email"), follow_redirects=True, ) uid = requests[0]["user"].fs_uniquifier code = requests[0]["login_token"] # Try bad/unknown id response = client.get("us-verify-link?id=98765&code=12345", follow_redirects=True) assert get_message("GENERIC_AUTHN_FAILED") in response.data # Try bad code response = client.get( f"us-verify-link?id={uid}&code=12345", follow_redirects=True, ) assert get_message("GENERIC_AUTHN_FAILED") in response.data # Try deactivated user with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") app.security.datastore.deactivate_user(user) app.security.datastore.commit() response = client.get( f"us-verify-link?id={uid}&code={code}", follow_redirects=True, ) assert get_message("GENERIC_AUTHN_FAILED") in response.data @pytest.mark.settings(url_prefix="/auth", us_signin_replaces_login=True) def test_propagate_next(app, client): # verify we propagate the ?next param all the way through a unified signin # Also test blueprint prefix since we rarely actually test that. set_phone(app) with capture_send_code_requests() as codes: response = client.get("profile", follow_redirects=True) assert "?next=/profile" in response.request.url signin_url = get_form_action(response, 0) sendcode_url = get_form_action(response, 1) response = client.post( sendcode_url, data=dict(identity="matt@lp.com", chosen_method="sms") ) data = dict(identity="matt@lp.com", passcode=codes[0]["login_token"]) response = client.post(signin_url, data=data, follow_redirects=False) assert "/profile" in response.location @pytest.mark.two_factor() @pytest.mark.settings(url_prefix="/auth", us_signin_replaces_login=True) def test_propagate_next_tf(app, client): # test next is propagated with a second factor response = client.post( "/auth/login", json=dict(identity="matt@lp.com", passcode="password") ) sms_sender = setup_tf_sms(client, url_prefix=app.config["SECURITY_URL_PREFIX"]) logout(client, endpoint="/auth/logout") response = client.get("/profile", follow_redirects=True) assert "?next=/profile" in response.request.url signin_url = get_form_action(response, 0) response = client.post( signin_url, data=dict(identity="matt@lp.com", passcode="password"), follow_redirects=True, ) sendcode_url = get_form_action(response, 0) response = client.post( sendcode_url, data=dict(code=sms_sender.messages[0].split()[-1]), follow_redirects=True, ) assert b"Profile Page" in response.data # Try form.next logout(client, endpoint="/auth/logout") response = client.post( "/auth/us-signin", data=dict(identity="matt@lp.com", passcode="password", next="/im-in"), follow_redirects=True, ) sendcode_url = get_form_action(response, 0) response = client.post( sendcode_url, data=dict(code=sms_sender.messages[0].split()[-1]), follow_redirects=False, ) assert "/im-in" in response.location @pytest.mark.babel(babel_default_locale="cic_US", test_xlations=True) def test_xlation(app, client, get_message_local): # Test setup method translation set_email(app) us_authenticate(client) response = client.get("us-setup") with app.test_request_context(): assert markupsafe.escape("SMS").encode() in response.data p = [ "Currently active sign in options: email OKAY!, password OKAY!. OKAY!", "Currently active sign in options: password OKAY!, email OKAY!. OKAY!", ] assert any(markupsafe.escape(s).encode() in response.data for s in p) @pytest.mark.registerable() @pytest.mark.settings(password_required=False) @pytest.mark.babel(babel_default_locale="cic_US", test_xlations=True) def test_empty_password_xlate(app, client, get_message): # test that if no password (and no other setup method) we get correct xlated # template data = dict(email="trp@lp.com", password="", password_confirm="") # register w/o password - this will automatically set up 'email' client.post("/register", data=data, follow_redirects=True) # will be auto-logged in since no confirmation response = client.get("us-setup") with app.test_request_context(): assert ( markupsafe.escape( "Currently active sign in options: email OKAY!. OKAY!" ).encode() in response.data ) # white-box testing - there are 2 places in us-setup code that set active methods response = client.post("us-setup", data=dict(delete_method="email")) with app.test_request_context(): assert ( markupsafe.escape( "Currently active sign in options: none OKAY!. OKAY!" ).encode() in response.data ) response = client.get("us-setup") with app.test_request_context(): assert ( markupsafe.escape( "Currently active sign in options: none OKAY!. OKAY!" ).encode() in response.data ) @pytest.mark.two_factor() @pytest.mark.csrf(csrfprotect=True) @pytest.mark.settings(CSRF_COOKIE_NAME="XSRF-Token") def test_csrf_2fa_us_cookie(app, client): # Use XSRF-Token cookie for entire login sequence sms_sender = SmsSenderFactory.createSender("test") response = client.get( "/us-signin", data={}, headers={"Content-Type": "application/json"} ) assert client.get_cookie("XSRF-Token") csrf_token = response.json["response"]["csrf_token"] assert csrf_token == client.get_cookie("XSRF-Token").value # verify requires CSRF response = client.post( "/us-signin", json=dict(identity="gal@lp.com", passcode="password"), headers={"Content-Type": "application/json"}, ) assert response.status_code == 400 assert response.json["response"]["errors"][0] == "The CSRF token is missing." response = client.post( "/us-signin", json=dict(identity="gal@lp.com", passcode="password"), headers={ "Content-Type": "application/json", "X-CSRF-Token": client.get_cookie("XSRF-Token").value, }, ) assert b'"code": 200' in response.data session = get_session(response) assert session["tf_state"] == "ready" assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post( "/tf-validate", json=dict(code=code), headers={ "Content-Type": "application/json", "X-CSRF-Token": client.get_cookie("XSRF-Token").value, }, ) assert response.status_code == 200 # verify original session csrf_token still works. response = client.post( "/json_auth", json=dict(label="label"), headers={"Content-Type": "application/json", "X-CSRF-Token": csrf_token}, ) assert response.status_code == 200 # use XSRF_Cookie to send in csrf_token response = client.post( "/json_auth", json=dict(label="label"), headers={ "Content-Type": "application/json", "X-CSRF-Token": client.get_cookie("XSRF-Token").value, }, ) assert response.status_code == 200 assert response.json["label"] == "label" @pytest.mark.settings(us_enabled_methods=["password", "email"]) def test_us_setup_email(app, client, get_message): set_email(app) us_authenticate(client) chosen_methods_choices = [] def recorder(template, us_setup_form, **kwargs): chosen_methods_choices.extend(us_setup_form.chosen_method.choices) return "ok" app.security.render_template = recorder client.get("/us-setup") assert len(chosen_methods_choices) == 0 @pytest.mark.settings(us_enabled_methods=["email", "authenticator"]) def test_us_setup_authenticator(app, client, get_message): set_email(app) us_authenticate(client) chosen_methods_choices = [] def recorder(template, us_setup_form, **kwargs): chosen_methods_choices.extend(us_setup_form.chosen_method.choices) return "ok" app.security.render_template = recorder client.get("/us-setup") assert len(chosen_methods_choices) == 1 assert chosen_methods_choices[0][0] == "authenticator" @pytest.mark.settings(us_enabled_methods=["email", "sms"]) def test_us_setup_sms(app, client, get_message): set_email(app) us_authenticate(client) chosen_methods_choices = [] def recorder(template, us_setup_form, **kwargs): chosen_methods_choices.extend(us_setup_form.chosen_method.choices) return "ok" app.security.render_template = recorder client.get("/us-setup") assert len(chosen_methods_choices) == 1 assert chosen_methods_choices[0][0] == "sms" @pytest.mark.settings(us_enabled_methods=["email", "sms", "authenticator"]) def test_us_setup_authenticator_sms(app, client, get_message): set_email(app) us_authenticate(client) chosen_methods_choices = [] def recorder(template, us_setup_form, **kwargs): chosen_methods_choices.extend(us_setup_form.chosen_method.choices) return "ok" app.security.render_template = recorder client.get("/us-setup") assert len(chosen_methods_choices) == 2 assert chosen_methods_choices[0][0] == "authenticator" assert chosen_methods_choices[1][0] == "sms" def _tf_required(self, tf_setup_methods, tf_fresh): if self.email == "gal@lp.com": return False, tf_setup_methods return True, tf_setup_methods @pytest.mark.two_factor() @pytest.mark.app_settings(TESTING_USER_INJECT=dict(check_tf_required=_tf_required)) @pytest.mark.settings(two_factor_required=True) def test_override_tf_required(app, client, get_message): data = dict(identity="jill@lp.com", passcode="password") response = client.post("/us-signin", json=data) assert response.status_code == 200 assert response.json["response"]["tf_required"] data = dict(identity="gal@lp.com", passcode="password") response = client.post("/us-signin", json=data) assert response.status_code == 200 assert not response.json["response"]["tf_required"] def _locked(self, form_error): if self.email == "gal@lp.com": form_error.append("You are not allowed to do that") return True return False @pytest.mark.app_settings(TESTING_USER_INJECT=dict(is_locked=_locked)) def test_override_user_locked(app, client, get_message): data = dict(identity="jill@lp.com", passcode="password") response = client.post("/us-signin", json=data) assert response.status_code == 200 logout(client) data = dict(identity="gal@lp.com", passcode="password") response = client.post("/us-signin", json=data) assert response.status_code == 400 assert response.json["response"]["errors"] == ["You are not allowed to do that"] assert response.json["response"]["field_errors"]["identity"] == [ "You are not allowed to do that" ] @pytest.mark.app_settings(TESTING_USER_INJECT=dict(is_locked=_locked)) @pytest.mark.settings(return_generic_responses=True) def test_override_user_locked_gr(app, client, get_message): data = dict(identity="gal@lp.com", passcode="password") response = client.post("/us-signin", json=data) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "GENERIC_AUTHN_FAILED" ) assert "identity" not in response.json["response"]["field_errors"] @pytest.mark.settings(default_remember_me=True) def test_remember_login_form(app, client): # ensure form has the checkbox set if default set. response = client.get("/us-signin") assert response.status_code == 200 r = get_form_input(response, "remember") assert "checked" in r pallets-eco-flask-security-72bf929/tests/test_utils.py000066400000000000000000000363741525721743500231760ustar00rootroot00000000000000""" test_utils ~~~~~~~~~~ Test utils :copyright: (c) 2019-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from __future__ import annotations from contextlib import contextmanager import re import time from flask.json.tag import TaggedJSONSerializer from flask.signals import message_flashed from flask_security import ( Security, SmsSenderBaseClass, SmsSenderFactory, UserMixin, ) from flask_security.signals import ( login_instructions_sent, reset_password_instructions_sent, tf_security_token_sent, user_registered, us_security_token_sent, ) from flask_security.utils import hash_data, hash_password from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer from werkzeug.http import parse_cookie _missing = object def authenticate( client, email="matt@lp.com", password="password", endpoint=None, csrf=False, remember=False, **kwargs, ): data = dict(email=email, password=password) if csrf: response = client.get(endpoint or "/login") data["csrf_token"] = get_form_input_value(response, "csrf_token") if remember: data["remember"] = "y" return client.post(endpoint or "/login", data=data, **kwargs) def json_authenticate( client, email="matt@lp.com", password="password", endpoint=None, headers=None ): data = dict(email=email, password=password) # Get auth token always ep = endpoint or "/login?include_auth_token" return client.post(ep, content_type="application/json", json=data, headers=headers) def is_authenticated(client, get_message, auth_token=None): # Return True is 'client' is authenticated. # Return False if not # Raise ValueError not certain... headers = {"accept": "application/json"} if auth_token: headers["Authentication-Token"] = auth_token response = client.get("/profile", headers=headers) if response.status_code == 200: return True if response.status_code == 401 and response.json["response"]["errors"][0].encode( "utf-8" ) == get_message("UNAUTHENTICATED"): return False raise ValueError("Failed to figure out if authenticated") def check_location(app, location, expected_base): # verify response location. Historically this can be absolute or relative based # on configuration. As of 5.4 and Werkzeug 2.1 it is always relative return location == expected_base def verify_token(client_nc, token, status=None): # Use passed auth token in API that requires auth and verify status. # Pass in a client_nc to get valid results. response = client_nc.get( "/token", headers={"Content-Type": "application/json", "Authentication-Token": token}, ) # /token is an application endpoint - SECURITY_CACHE_CONTROL must not # be applied to it (see issue #1263). assert "Cache-Control" not in response.headers if status: assert response.status_code == status else: assert b"Token Authentication" in response.data def logout(client, endpoint=None, **kwargs): return client.post(endpoint or "/logout", **kwargs) def json_logout(client, token, endpoint=None): return client.post( endpoint or "/logout", headers={ "Content-Type": "application/json", "Accept": "application/json", "Authentication-Token": token, }, ) def get_csrf_token(client): response = client.get( "/login", data={}, headers={"Accept": "application/json"}, ) return response.json["response"]["csrf_token"] def get_session(response): """Return session cookie contents. This a base64 encoded json. Returns a dict """ # Alas seems like if there are multiple set-cookie headers - we are on our own for index, h in enumerate(response.headers): if h[0] == "Set-Cookie": cookie = parse_cookie(response.headers[index][1]) encoded_cookie = cookie.get("session", None) if encoded_cookie: serializer = URLSafeTimedSerializer( "secret", serializer=TaggedJSONSerializer() ) val = serializer.loads_unsafe(encoded_cookie) return val[1] def setup_tf_sms(client, url_prefix=None, csrf_token=None): # Simple setup of SMS as a second factor and return the sender so caller # can get codes. SmsSenderFactory.senders["test"] = SmsTestSender sms_sender = SmsSenderFactory.createSender("test") data = dict(setup="sms", phone="+442083661188", csrf_token=csrf_token) response = client.post("/".join(filter(None, (url_prefix, "tf-setup"))), json=data) assert sms_sender.get_count() == 1 code = sms_sender.messages[0].split()[-1] response = client.post( "/".join(filter(None, (url_prefix, "tf-validate"))), json=dict(code=code, csrf_token=csrf_token), ) assert response.status_code == 200 return sms_sender def get_existing_session(client): cookie = client.get_cookie("session") if cookie: serializer = URLSafeTimedSerializer("secret", serializer=TaggedJSONSerializer()) val = serializer.loads_unsafe(cookie.value) return val[1] return None def reset_fresh(client, within): # Assumes client authenticated. # Upon return the NEXT request if protected with a freshness check # will require a fresh authentication. with client.session_transaction() as sess: old_paa = sess["fs_paa"] - within.total_seconds() - 100 sess["fs_paa"] = old_paa sess.pop("fs_gexp", None) return old_paa def reset_fresh_auth_token(app, within, email="matt@lp.com"): # Assumes client authenticated. # Returns a new auth token that will force the NEXT request, # if protected with a freshness check to require a fresh authentication with app.test_request_context("/"): user = app.security.datastore.find_user(email=email) tdata = dict(ver=str(5)) if hasattr(user, "fs_token_uniquifier"): tdata["uid"] = str(user.fs_token_uniquifier) else: tdata["uid"] = str(user.fs_uniquifier) tdata["fs_paa"] = time.time() - within.total_seconds() - 100 tdata["exp"] = int(app.config.get("SECURITY_TOKEN_EXPIRE_TIMESTAMP")(user)) return app.security.remember_token_serializer.dumps(tdata) def get_form_action(response, ordinal=0): # Return the URL that the form WOULD post to - this is useful to check # how our templates actually work (e.g. propagation of 'next') matcher = re.findall( r'(?:
    ]*id="{field_id}"[^>]*)">') def get_form_input_value(response, field_id): # return 'value' of field with the id == field_id or None if not found return _parse_form_input( response, f']*id="{field_id}"[^>]*value="([^"]*)">' ) def check_xlation(app, locale): """Return True if locale is loaded""" with app.test_request_context(): domain = app.security.i18n_domain xlations = domain.get_translations() if not xlations: return False # Flask-Babel doesn't populate _info as Flask-BabelEx did - so look in first # string which is catalog info. matcher = re.search(r"Language:\s*(\w+)", xlations._catalog[""]) return matcher.group(1) == locale def create_roles(ds): roles = [ ("admin", ["full-read", "full-write", "super"]), ("editor", ["full-read", "full-write"]), ("author", ["full-read", "my-write"]), ("simple", None), ] for role in roles: if hasattr(ds.role_model, "permissions") and role[1]: ds.create_role(name=role[0], permissions=role[1]) else: ds.create_role(name=role[0]) ds.commit() def create_users(app, ds, count=None): users = [ ("matt@lp.com", "matt", "password", ["admin"], True, 123456, None), ("joe@lp.com", "joe", "password", ["editor"], True, 234567, None), ("dave@lp.com", "dave", "password", ["admin", "editor"], True, 345678, None), ("jill@lp.com", "jill", "password", ["author"], True, 456789, None), ("tiya@lp.com", "tiya", "password", [], False, 567890, None), ("gene@lp.com", "gene", "password", ["simple"], True, 889900, None), ("jess@lp.com", "jess", None, [], True, 678901, None), ("gal@lp.com", "gal", "password", ["admin"], True, 112233, "sms"), ("gal2@lp.com", "gal2", "password", ["admin"], True, 223311, "authenticator"), ("gal3@lp.com", "gal3", "password", ["admin"], True, 331122, "email"), ] count = count or len(users) for u in users[:count]: pw = u[2] if pw is not None: pw = hash_password(pw) roles = [ds.find_or_create_role(rn) for rn in u[3]] ds.commit() totp_secret = None if app.config.get("SECURITY_TWO_FACTOR", None) and u[6]: totp_secret = app.security._totp_factory.generate_totp_secret() phone_number = "650-277-7098" if u[6] == "sms" else None user = ds.create_user( email=u[0], username=u[1], password=pw, active=u[4], security_number=u[5], tf_primary_method=u[6], tf_totp_secret=totp_secret, tf_phone_number=phone_number, ) ds.commit() for role in roles: ds.add_role_to_user(user, role) ds.commit() def populate_data(app, user_count=None): ds = app.security.datastore with app.app_context(): create_roles(ds) create_users(app, ds, user_count) def init_app_with_options(app, datastore, **options): security_args = options.pop("security_args", {}) app.config.update(**options) app.security = Security(app, datastore=datastore, **security_args) populate_data(app) @contextmanager def capture_queries(datastore): from sqlalchemy import event queries = [] @event.listens_for(datastore.db.session, "do_orm_execute") def _do_orm_execute(orm_execute_state): queries.append(orm_execute_state) yield queries class SmsTestSender(SmsSenderBaseClass): messages: list[str] = [] count = 0 # This looks strange because we need class variables since test need to access a # sender but the actual sender is instantiated low down in SMS code. def __init__(self): super().__init__() SmsTestSender.count = 0 SmsTestSender.messages = [] def send_sms(self, from_number, to_number, msg): SmsTestSender.messages.append(msg) SmsTestSender.count += 1 return def get_count(self): return SmsTestSender.count class SmsBadSender(SmsSenderBaseClass): def send_sms(self, from_number, to_number, msg): raise ValueError(f"Unknown number: {to_number}") @contextmanager def capture_passwordless_login_requests(): login_requests = [] def _on(app, **data): login_requests.append(data) login_instructions_sent.connect(_on) try: yield login_requests finally: login_instructions_sent.disconnect(_on) @contextmanager def capture_registrations(): """Testing utility for capturing registrations.""" registrations = [] def _on(app, **data): data["email"] = data["user"].email registrations.append(data) user_registered.connect(_on) try: yield registrations finally: user_registered.disconnect(_on) @contextmanager def capture_reset_password_requests(): """Testing utility for capturing password reset requests.""" reset_requests = [] def _on(app, **data): reset_requests.append(data) reset_password_instructions_sent.connect(_on) try: yield reset_requests finally: reset_password_instructions_sent.disconnect(_on) def check_signals( signals: dict, expected: str | list | dict[str, int] | None = None, ignore: str | list[str] | None = None, ) -> None: # Check if test received expected # of signals. # Expected can be a string (signal name), list of strings or # dict of signal name to expected count. # Use with the signals fixture if not expected: expected = dict() elif isinstance(expected, str): expected = {expected: 1} elif isinstance(expected, list): expected = {sig: 1 for sig in expected} if not ignore: ignore = ["user_authenticated"] # this is too common in tests to worry about elif isinstance(ignore, str): ignore = [ignore] for sig in signals: if sig not in ignore: expected_calls = expected.get(sig, 0) actual_calls = len(signals[sig]) assert ( expected_calls == actual_calls ), f"Expected {expected_calls} calls to {sig} but got {actual_calls}" @contextmanager def capture_flashes(): """Testing utility for capturing flashes.""" flashes = [] def _on(app, **data): flashes.append(data) message_flashed.connect(_on) try: yield flashes finally: message_flashed.disconnect(_on) @contextmanager def capture_send_code_requests(): # Easy way to get token/code required for code logins # either second factor or us_signin login_requests = [] def _on(app, **data): assert all(v in data for v in ["user", "method", "login_token"]) assert isinstance(data["user"], UserMixin) login_requests.append(data) us_security_token_sent.connect(_on) tf_security_token_sent.connect(_on) try: yield login_requests finally: us_security_token_sent.disconnect(_on) tf_security_token_sent.disconnect(_on) def get_auth_token_version_3x(app, user): """ Copy of algorithm that generated user token in version 3.x """ data = [str(user.id), hash_data(user.password)] if hasattr(user, "fs_uniquifier"): data.append(user.fs_uniquifier) return app.security.remember_token_serializer.dumps(data) def get_auth_token_version_4x(app, user): """Copy of algorithm that generated user token in version 4.x- 5.4""" data = [str(user.fs_uniquifier)] return app.security.remember_token_serializer.dumps(data) class FakeSerializer: def __init__(self, age=None, invalid=False): self.age = age self.invalid = invalid def loads(self, token, max_age): if self.age: assert max_age == self.age raise SignatureExpired("expired") if self.invalid: raise BadSignature("bad") def loads_unsafe(self, token): return None, None def dumps(self, state): return "heres your state" def convert_bool_option(v): # Used for command line options to convert string to bool if str(v).lower() in ["true"]: return True elif str(v).lower() in ["false"]: return False return v pallets-eco-flask-security-72bf929/tests/test_webauthn.py000066400000000000000000002174621525721743500236520ustar00rootroot00000000000000""" test_webauthn ~~~~~~~~~~~~~~~~~~~ WebAuthn tests :copyright: (c) 2021-2026 by J. Christopher Wagner (jwag). :license: MIT, see LICENSE for more details. """ from base64 import urlsafe_b64encode import copy import datetime from datetime import timedelta import json import re import pytest from tests.test_two_factor import tf_in_session from tests.test_utils import ( FakeSerializer, authenticate, capture_flashes, check_location, get_existing_session, get_form_action, get_form_input_value, is_authenticated, json_authenticate, logout, reset_fresh, setup_tf_sms, verify_token, check_signals, get_form_input, ) from flask_security import ( WebauthnUtil, user_authenticated, wan_registered, wan_deleted, ) # We can't/don't test the actual client-side JavaScript and browser APIs - so # to create reproducible tests, use view_scaffold, set breakpoints in the views and # cut-and-paste the responses. That requires that 'challenge' and 'rp_origin' be # identical between view_scaffold and tests here. CHALLENGE = "smCCiy_k2CqQydSQ_kPEjV5a2d0ApfatcpQ1aXDmQPo" REG_DATA1 = { "id": "wUUqNOjY35dcT-vpikZpZx-T91NjIe4PqrV8j7jYPOc", "rawId": "wUUqNOjY35dcT-vpikZpZx-T91NjIe4PqrV8j7jYPOc", "type": "public-key", "response": { "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikSZYN5YgOjGh0NB" "cPZHZgW4_krrmihjLHmVzzuoMdl2NFAAAAAQAAAAAAAAAAAAAAAAAAA" "AAAIMFFKjTo2N-XXE_r6YpGaWcfk_dTYyHuD6q1fI-42DznpQECAy" "YgASFYIFRipoWMEiDuCtLUvSlqCFZBqxvUuNqZKavlWgvN2BK8Il" "ggLOV4eez9k0det5oIZGyKanGkmWa0hygnjjFmf8Rep6c", "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYzIxR" "FEybDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UVhCbVlYUmpjRk" "V4WVZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NT" "AwMSIsImNyb3NzT3JpZ2luIjpmYWxzZX0", "transports": ["usb"], }, "extensions": '{"credProps": {}}', } SIGNIN_DATA1 = { "id": "wUUqNOjY35dcT-vpikZpZx-T91NjIe4PqrV8j7jYPOc", "rawId": "wUUqNOjY35dcT-vpikZpZx-T91NjIe4PqrV8j7jYPOc", "type": "public-key", "response": { "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MBAAAABQ==", "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiYzIxRFEy" "bDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UVhCbVlYUmpjRkV4" "WVZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAw" "MSIsImNyb3NzT3JpZ2luIjpmYWxzZX0=", "signature": "MEUCIH5VdRXxfnoxfrVk72gvWAn91QH-l2UrIohk5YOWi9XpAiEAn6f9oHtFS" "68HVf6K_Ku0L33C0sID2HzpJWSiTNgJlbU=", }, "assertionClientExtensions": "{}", } REG_DATA2 = { "id": "lpMv8FTVHVSxteQJ3N4azlSxXiBJADA7IK-NleETceZYODy51_Cqt7Rx6pfVP1BI", "rawId": "lpMv8FTVHVSxteQJ3N4azlSxXiBJADA7IK-NleETceZYODy51_Cqt7Rx6pfVP1BI", "type": "public-key", "response": { "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjCSZYN5YgOj" "Gh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2PFAAAAAgAAAAAAAAAAA" "AAAAAAAAAAAMJaTL_BU1R1UsbXkCdzeGs5UsV4gSQAwOyCvjZXhE3" "HmWDg8udfwqre0ceqX1T9QSKUBAgMmIAEhWCCWky_wVNUdVL" "G15AncLU8mBQCtY10BjnSDoOUlRjkU1CJYIIA1U9vNDpZ" "TihC2x0CxRZ-trF_zazYosuEqYdHSOIjZoWtjcmVkUHJvdGVjdAI", "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoi" "YzIxRFEybDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UV" "hCbVlYUmpjRkV4WVZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6L" "y9sb2NhbGhvc3Q6NTAwMSIsImNyb3NzT3JpZ2luIjpmYWxzZX0", "transports": ["nfc", "usb"], }, "extensions": '{"credProps": {"rk": True}}', } # This has user_verification=True - i.e. a multi-factor capable key REG_DATA_UV = { "id": "s3xZpfGy0ZH-sSkfxIsgChwbkw_O0jOFtZeJ1LXUMEa8atG1oEskNqmFJCfgKZGy", "rawId": "s3xZpfGy0ZH-sSkfxIsgChwbkw_O0jOFtZeJ1LXUMEa8atG1oEskNqmFJCfgKZGy", "type": "public-key", "response": { "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjC" "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2PFAAAABAAAAA" "AAAAAAAAAAAAAAAAAAMLN8WaXxstGR_rEpH8SLIAocG5MPztIzhbWXi" "dS11DBGvGrRtaBLJDaphSQn4CmRsqUBAgMmIAEhWCCzfFml8bLRkf" "6xKR_EUnaoI333MuxRlv5-LwojDibdTyJYIFMifFwn-RfkDDgsTHF" "jWgE6bld-Jc4nhFMTkQja9P8IoWtjcmVkUHJvdGVjdAI", "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYzI" "xRFEybDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UVhCbVlY" "UmpjRkV4WVZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2Nhb" "Ghvc3Q6NTAwMSIsImNyb3NzT3JpZ2luIjpmYWxzZX0", "transports": ["nfc", "usb"], }, "extensions": '{"credProps":{"rk":true}}', } SIGNIN_DATA_UV = { "id": "s3xZpfGy0ZH-sSkfxIsgChwbkw_O0jOFtZeJ1LXUMEa8atG1oEskNqmFJCfgKZGy", "rawId": "s3xZpfGy0ZH-sSkfxIsgChwbkw_O0jOFtZeJ1LXUMEa8atG1oEskNqmFJCfgKZGy", "type": "public-key", "response": { "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABQ==", "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiYzIxRFEy" "bDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UVhCbVlYUmpjRkV4W" "VZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6NTAwMSI" "sImNyb3NzT3JpZ2luIjpmYWxzZX0=", "signature": "MEUCIQDR0m9Ob4nqVGiAPUf1Tu5XohDh2frl1LJ6G41GURlUIgIgKUPfkw" "AjP2863L2nDhcR2EKqoGEQLqlQ5xymZstyO6o=", }, "assertionClientExtensions": "{}", } REG_DATA_UH = { "id": "rHb1OXVM--dgGcWg0u3cfomyc-Tu4l4kK8GjVkS8bms-foXmBAlWHyTzuhgGgCnx", "rawId": "rHb1OXVM--dgGcWg0u3cfomyc-Tu4l4kK8GjVkS8bms-foXmBAlWHyTzuhgGgCnx", "type": "public-key", "response": { "attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVjCSZYN5YgOjGh0NBc" "PZHZgW4_krrmihjLHmVzzuoMd" "l2PFAAAAAgAAAAAAAAAAAAAAAAAAAAAAMKx29Tl1TPvnYBnFoNLt3H6J" "snPk7uJeJCvBo1ZEvG5rPn6F" "5gQJVh8k87oYBoAp8aUBAgMmIAEhWCCsdvU5dUz752AZxaDSyN-ocBL" "Bo99GevEWTnUxSkMRICJYIILE" "DLF8cQNM5l6ZgDxIYpvU88xgbq44lmR6oCBbNaHhoWtjcmVkUHJvdG" "VjdAI", "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiYzIx" "RFEybDVYMnN5UTNGUmVXUlRVVjl" "yVUVWcVZqVmhNbVF3UVhCbVlYUmpjRkV4WVZoRWJWRlFidyIsIm9yaWdpbi" "I6Imh0dHA6Ly9sb2NhbGhvc3" "Q6NTAwMSIsImNyb3NzT3JpZ2luIjpmYWxzZX0", "transports": ["nfc", "usb"], }, "extensions": '{"credProps":{"rk": true}}', } SIGNIN_DATA_UH = { "id": "rHb1OXVM--dgGcWg0u3cfomyc-Tu4l4kK8GjVkS8bms-foXmBAlWHyTzuhgGgCnx", "rawId": "rHb1OXVM--dgGcWg0u3cfomyc-Tu4l4kK8GjVkS8bms-foXmBAlWHyTzuhgGgCnx", "type": "public-key", "response": { "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2MFAAAABQ==", "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiYzIxRFEy" "bDVYMnN5UTNGUmVXUlRVVjlyVUVWcVZqVmhNbVF3UVhCbVlYUmpjR" "kV4WVZoRWJWRlFidyIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q" "6NTAwMSIsImNyb3NzT3JpZ2luIjpmYWxzZX0=", "signature": "MEUCIQCbKwaQv_GzrWfc0nVXqhe6WZs_5Sb2b7xHC9iDW9aHeQIgF2PlfM7FdyV" "xcPhofekJjLBgDMTbK4mwIWHgiExZ54s=", "userHandle": "NTgxMTU3YmM2MGU3NGM1OTg0OTBjYTI1ZTgwNjc4MDY=", }, "assertionClientExtensions": "{}", } class HackWebauthnUtil(WebauthnUtil): def generate_challenge(self, nbytes=None): return CHALLENGE def origin(self): # This is from view_scaffold return "http://localhost:5001" pytestmark = pytest.mark.webauthn(webauthn_util_cls=HackWebauthnUtil) def _register_start( client, name="testr1", usage="secondary", endpoint="wan-register", csrf_token=None ): response = client.post( endpoint, data=dict(name=name, usage=usage, csrf_token=csrf_token) ) matcher = re.match( r".*handleRegister\(\'(.*)\'\).*", response.data.decode("utf-8"), re.IGNORECASE | re.DOTALL, ) register_options = json.loads(matcher.group(1)) response_url = get_form_action(response) return register_options, response_url def _register_start_json(client, name="testr1", usage="secondary", csrf_token=None): response = client.post( "wan-register", json=dict(name=name, usage=usage, csrf_token=csrf_token) ) register_options = response.json["response"]["credential_options"] response_url = f'wan-register/{response.json["response"]["wan_state"]}' return register_options, response_url def reg_2_keys(client): # Register 2 keys - one first, one secondary # This can be used by other tests outside this module. authenticate(client) register_options, response_url = _register_start_json( client, name="first", usage="first" ) response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA_UV))) assert response.status_code == 200 register_options, response_url = _register_start_json( client, name="second", usage="secondary" ) response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 return { "first": {"id": REG_DATA_UV["id"], "signin": SIGNIN_DATA_UV}, "secondary": {"id": REG_DATA1["id"], "signin": SIGNIN_DATA1}, } def reg_first_key(client, csrf_token=None): # Register a primary key - assumes already authenticated # This can be used by other tests outside this module. register_options, response_url = _register_start_json( client, name="first", usage="first", csrf_token=csrf_token ) response = client.post( response_url, json=dict(credential=json.dumps(REG_DATA_UV), csrf_token=csrf_token), ) assert response.status_code == 200 return {"id": REG_DATA_UV["id"], "signin": SIGNIN_DATA_UV} def _signin_start( client, identity=None, endpoint="wan-signin", csrf_token=None, ): response = client.post( endpoint, data=dict(identity=identity, csrf_token=csrf_token) ) matcher = re.match( r".*handleSignin\(\'(.*)\'\).*", response.data.decode("utf-8"), re.IGNORECASE | re.DOTALL, ) signin_options = json.loads(matcher.group(1)) response_url = get_form_action(response) return signin_options, response_url def _signin_start_json(client, identity=None, remember=False, endpoint="wan-signin"): headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.post( endpoint, headers=headers, json=dict(identity=identity, remember=remember) ) signin_options = response.json["response"]["credential_options"] response_url = f'wan-signin/{response.json["response"]["wan_state"]}' return signin_options, response_url, response.json def wan_signin(client, identity, signin_data, wan_signin_url): # perform complete sign in - useful for tests outside this module. signin_options, response_url = _signin_start( client, identity, endpoint=wan_signin_url ) response = client.post( response_url, data=dict(credential=json.dumps(signin_data)), follow_redirects=True, ) assert response.status_code == 200 return response def reset_signcount(app, email, keyname): # Due to replay attack prevention, we can only use a key once since the server # increments the sign_count and we can't do that on the client side! with app.app_context(): user = app.security.datastore.find_user(email=email) cred = [c for c in user.webauthn if c.name == keyname][0] cred.sign_count = cred.sign_count - 1 app.security.datastore.put(cred) app.security.datastore.commit() def test_basic(app, clients, get_message): auths = [] @user_authenticated.connect_via(app) def authned(myapp, user, **extra_args): auths.append((user.email, extra_args["authn_via"])) @wan_registered.connect_via(app) def pc(sender, user, name, **extra_args): assert name == "testr1" assert len(user.webauthn) == 1 authenticate(clients) response = clients.get("/wan-register") # default config allows for both primary and secondary usage # so form should have selector assert get_form_input_value(response, "usage-0") assert get_form_input_value(response, "usage-1") # post with no name response = clients.post("/wan-register", data=dict()) assert get_message("WEBAUTHN_NAME_REQUIRED") in response.data register_options, response_url = _register_start(clients, usage="first") assert register_options["rp"]["name"] == "My Flask App" assert register_options["user"]["name"] == "matt@lp.com" assert not register_options["excludeCredentials"] assert register_options["authenticatorSelection"]["residentKey"] == "preferred" assert register_options["extensions"]["credProps"] # Register using the static data above response = clients.post( response_url, data=dict(credential=json.dumps(REG_DATA1)), follow_redirects=True ) assert response.status_code == 200 assert get_message("WEBAUTHN_REGISTER_SUCCESSFUL", name="testr1") in response.data assert b"testr1" in response.data # sign in - simple case use identity so we get back allowCredentials logout(clients) signin_options, response_url = _signin_start(clients, "matt@lp.com") assert signin_options["timeout"] == app.config["SECURITY_WAN_SIGNIN_TIMEOUT"] assert signin_options["userVerification"] == "preferred" allow_credentials = signin_options["allowCredentials"] assert len(allow_credentials) == 1 assert allow_credentials[0]["id"] == REG_DATA1["id"] assert allow_credentials[0]["transports"] == ["usb"] response = clients.post( response_url, data=dict(credential=json.dumps(SIGNIN_DATA1)), follow_redirects=True, ) assert response.status_code == 200 assert b"Welcome matt@lp.com" in response.data assert len(auths) == 2 assert auths[1][1] == ["webauthn"] # verify actually logged in response = clients.get("/profile", follow_redirects=False) assert response.status_code == 200 def test_basic_json(app, clients, get_message): headers = {"Accept": "application/json", "Content-Type": "application/json"} auths = [] @user_authenticated.connect_via(app) def authned(myapp, user, **extra_args): auths.append((user.email, extra_args["authn_via"])) authenticate(clients) # post with no name response = clients.post("/wan-register", json=dict()) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_NAME_REQUIRED" ) register_options, response_url = _register_start_json(clients, usage="first") assert register_options["rp"]["name"] == "My Flask App" assert register_options["user"]["name"] == "matt@lp.com" # Register using the static data above response = clients.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 # reset lastuse_datetime so we can verify signing in correctly alters it fake_dt = datetime.datetime(2020, 4, 7, 9, 27) with app.app_context(): user = app.security.datastore.find_user(email="matt@lp.com") for cred in user.webauthn: cred.lastuse_datetime = fake_dt app.security.datastore.put(cred) app.security.datastore.commit() if hasattr(app.security.datastore.db, "close_db") and callable( app.security.datastore.db.close_db ): app.security.datastore.db.close_db(None) response = clients.get("/wan-register", headers=headers) active_creds = response.json["response"]["registered_credentials"] assert active_creds[0]["name"] == "testr1" assert datetime.datetime.fromisoformat(active_creds[0]["lastuse"]) == fake_dt # sign in - simple case use identity so we get back allowCredentials logout(clients) signin_options, response_url, rjson = _signin_start_json(clients, "matt@lp.com") assert signin_options["userVerification"] == "preferred" allow_credentials = signin_options["allowCredentials"] assert len(allow_credentials) == 1 assert allow_credentials[0]["id"] == REG_DATA1["id"] assert "user" not in rjson["response"] response = clients.post( f"{response_url}?include_auth_token", json=dict(credential=json.dumps(SIGNIN_DATA1)), ) assert response.status_code == 200 assert response.json["response"]["user"]["email"] == "matt@lp.com" assert auths[1][1] == ["webauthn"] # verify actually logged in assert is_authenticated(clients, get_message) verify_token(clients, response.json["response"]["user"]["authentication_token"]) # fetch credentials and verify lastuse was updated response = clients.get("/wan-register", headers=headers) active_creds = response.json["response"]["registered_credentials"] assert datetime.datetime.fromisoformat(active_creds[0]["lastuse"]) != fake_dt assert active_creds[0]["transports"] == ["usb"] assert active_creds[0]["usage"] == "first" logout(clients) # verify that unknown identities are just ignored when USER_HINTS is True response = clients.post("/wan-signin", json=dict(identity="whoami@lp.com")) assert response.status_code == 200 @pytest.mark.settings(wan_allow_user_hints=False) def test_basic_json_nohints(app, client, get_message): # Test that with no hints allowed, we don't get any credentials and we can still # sign in. authenticate(client) register_options, response_url = _register_start_json(client, usage="first") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 # With no hints we default to requiring a resident key # With allow as primary we default to requiring a cross-platform key assert ( register_options["authenticatorSelection"]["authenticatorAttachment"] == "cross-platform" ) assert register_options["authenticatorSelection"]["residentKey"] == "required" logout(client) signin_options, response_url, rjson = _signin_start_json(client, "matt@lp.com") allow_credentials = signin_options["allowCredentials"] assert len(allow_credentials) == 0 assert "user" not in rjson["response"] response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA1)), ) assert response.status_code == 200 assert response.json["response"]["user"]["email"] == "matt@lp.com" def test_usage(app, client, get_message): authenticate(client) register_options, response_url = _register_start_json(client, usage="secondary") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 logout(client) signin_options, response_url, _ = _signin_start_json(client, "matt@lp.com") response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA1)), ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_CREDENTIAL_WRONG_USAGE" ) def test_constraints(app, clients, get_message): """Test that nickname is unique for a given user but different users can have the same nickname. Also that credential_id is unique across the app. """ authenticate(clients) register_options, response_url = _register_start_json(clients, name="testr3") response = clients.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 # register same name again response = clients.post("wan-register", json=dict(name="testr3")) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_NAME_INUSE", name="testr3" ) logout(clients) # Different user - should get credential id in use error authenticate(clients, email="joe@lp.com") register_options, response_url = _register_start_json(clients, name="testr3") response = clients.post(response_url, json=dict(credential=json.dumps(REG_DATA2))) assert response.status_code == 200 # Try to register with identical credential ID as other user register_options, response_url = _register_start_json(clients, name="testr4") response = clients.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_CREDENTIAL_ID_INUSE" ) def test_bad_data_register(app, client, get_message): authenticate(client) register_options, response_url = _register_start_json(client, name="testr3") # first try mangling json - should get API_ERROR response = client.post(response_url, json=dict(credential='"hi there"')) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) response = client.post(response_url, json=dict(credential="")) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) # Now pass incorrect keys bad_register = copy.deepcopy(REG_DATA1) del bad_register["rawId"] response = client.post(response_url, json=dict(credential=json.dumps(bad_register))) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) # now muck with attestation - should get VERIFY ERROR bad_register = copy.deepcopy(REG_DATA1) bad_register["rawId"] = "unknown" response = client.post(response_url, json=dict(credential=json.dumps(bad_register))) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_NO_VERIFY", cause="id and raw_id were not equivalent" ) # same with forms with capture_flashes() as flashes: response = client.post( response_url, data=dict(credential=json.dumps(bad_register)), follow_redirects=False, ) assert response.status_code in [302, 303] assert "/wan-register" in response.location assert flashes[0]["category"] == "error" assert flashes[0]["message"].encode("utf-8") == get_message( "WEBAUTHN_NO_VERIFY", cause="id and raw_id were not equivalent" ) def test_bad_data_signin(app, client, get_message, signals): authenticate(client) register_options, response_url = _register_start_json(client, usage="first") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 logout(client) signin_options, response_url, _ = _signin_start_json(client, "matt@lp.com") response = client.post(response_url, json=dict(credential='"hi there"')) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) response = client.post(response_url, json=dict(credential="")) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) # Now pass incorrect keys bad_signin = copy.deepcopy(SIGNIN_DATA1) del bad_signin["rawId"] response = client.post(response_url, json=dict(credential=json.dumps(bad_signin))) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) # now muck with attestation - should get VERIFY ERROR bad_signin = copy.deepcopy(SIGNIN_DATA1) bad_signin["response"]["signature"] = bad_signin["response"]["signature"].replace( "M", "N" ) response = client.post(response_url, json=dict(credential=json.dumps(bad_signin))) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_NO_VERIFY", cause="Could not verify authentication signature" ) assert ( signals["user_failed_authn"][0]["request_endpoint"] == "security.wan_signin_response" ) assert signals["user_failed_authn"][0]["user"].email == "matt@lp.com" assert signals["user_failed_authn"][0]["auth_type"] == "passkey" assert not signals["user_failed_authn"][0]["tfa"] check_signals( signals, ["user_failed_authn", "wan_registered", "user_authenticated"] ) def test_delete(app, clients, get_message): @wan_deleted.connect_via(app) def pc(sender, user, name, **extra_args): assert name == "testr3" assert len(user.webauthn) == 1 authenticate(clients) register_options, response_url = _register_start(clients, name="testr3") response = clients.post( response_url, data=dict(credential=json.dumps(REG_DATA1)), follow_redirects=True ) assert response.status_code == 200 assert get_message("WEBAUTHN_REGISTER_SUCCESSFUL", name="testr3") in response.data response = clients.get("/wan-register") assert b"testr3" in response.data # Make sure GET works - this is important if we get a freshness redirect when # attempting to delete - the verify endpoint will redirect back to here. response = clients.get("/wan-delete", follow_redirects=False) assert response.status_code in [302, 303] """ response = clients.post("/wan-delete") assert get_message("WEBAUTHN_NAME_REQUIRED") in response.data """ response = clients.post( "/wan-delete", data=dict(name="testr1"), follow_redirects=True ) assert get_message("WEBAUTHN_NAME_NOT_FOUND", name="testr1") in response.data with capture_flashes() as flashes: response = clients.post( "/wan-delete", data=dict(name="testr3"), follow_redirects=True ) assert flashes[0]["category"] == "info" assert flashes[0]["message"].encode("utf-8") == get_message( "WEBAUTHN_CREDENTIAL_DELETED", name="testr3" ) response = clients.get("/wan-register") assert b"testr3" not in response.data def test_delete_json(app, clients, get_message): headers = {"Accept": "application/json", "Content-Type": "application/json"} authenticate(clients) register_options, response_url = _register_start_json(clients, name="testr3") response = clients.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 response = clients.get("/wan-register", headers=headers) active_creds = response.json["response"]["registered_credentials"] assert active_creds[0]["name"] == "testr3" response = clients.post("/wan-delete", json=dict()) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_NAME_REQUIRED" ) response = clients.post("/wan-delete", json=dict(name="testr1")) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_NAME_NOT_FOUND", name="testr1" ) response = clients.post("/wan-delete", json=dict(name="testr3")) assert response.status_code == 200 def test_disabled_account(app, client, get_message): # With USER_HINTS enabled, should get 200 on initial signin POST, but # not receive a list of registered credentials. authenticate(client) register_options, response_url = _register_start_json( client, name="testr3", usage="first" ) response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 logout(client) with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") app.security.datastore.deactivate_user(user) app.security.datastore.commit() signin_options, response_url = _signin_start(client, "matt@lp.com") assert response.status_code == 200 allow_credentials = signin_options["allowCredentials"] assert len(allow_credentials) == 0 # Now set USER_HINTS false and should get 400 on second POST app.config["SECURITY_WAN_ALLOW_USER_HINTS"] = False # Identity should be ignored signin_options, response_url = _signin_start(client, "matt@lp.com") allow_credentials = signin_options["allowCredentials"] assert len(allow_credentials) == 0 response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA1)), ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "DISABLED_ACCOUNT" ) def _locked(self, form_error): if self.email == "gal@lp.com": from flask import request if request.endpoint != "security.login": form_error.append("You are not allowed to do that") return True return False @pytest.mark.app_settings(TESTING_USER_INJECT=dict(is_locked=_locked)) def test_override_user_locked(app, client, get_message): authenticate(client, email="gal@lp.com") register_options, response_url = _register_start_json( client, name="testr3", usage="first" ) response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) logout(client) signin_options, response_url, _ = _signin_start_json(client, "gal@lp.com") response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA1)), ) assert response.status_code == 400 assert response.json["response"]["errors"] == ["You are not allowed to do that"] assert response.json["response"]["field_errors"]["credential"] == [ "You are not allowed to do that" ] def test_unk_credid(app, client, get_message): authenticate(client) register_options, response_url = _register_start_json( client, name="testr3", usage="first" ) response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 logout(client) signin_options, response_url, _ = _signin_start_json(client, "matt@lp.com") assert len(signin_options["allowCredentials"]) == 1 bad_signin = copy.deepcopy(SIGNIN_DATA1) bad_signin["rawId"] = bad_signin["rawId"].replace("w", "d") response = client.post( response_url, json=dict(credential=json.dumps(bad_signin)), ) assert response.status_code == 400 assert response.json["response"]["field_errors"]["credential"][0].encode( "utf-8" ) == get_message("WEBAUTHN_UNKNOWN_CREDENTIAL_ID") # same with forms with capture_flashes() as flashes: response = client.post( response_url, data=dict(credential=json.dumps(bad_signin)), follow_redirects=False, ) assert response.status_code in [302, 303] assert "/wan-signin" in response.location assert flashes[0]["category"] == "error" assert flashes[0]["message"].encode("utf-8") == get_message( "WEBAUTHN_UNKNOWN_CREDENTIAL_ID" ) @pytest.mark.settings(wan_allow_as_first_factor=False) def test_no_first_factor(app, client, get_message): # make sure that is app not configured to allow a webauthn key as a 'first' # authenticator, that the endpoint 'disappears'. authenticate(client) register_options, response_url = _register_start_json(client, name="testr3") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 logout(client) response = client.post("wan-signin", json=dict(identity="matt@lp.com")) assert response.status_code == 404 @pytest.mark.two_factor() @pytest.mark.unified_signin() def test_tf(app, client, get_message): # Test using webauthn key as a second factor # Register 2 keys - one "first" one "secondary" keys = reg_2_keys(client) logout(client) # log back in - should require MFA. response = client.post( "/us-signin", data=dict(identity="matt@lp.com", passcode="password", remember=True), follow_redirects=True, ) assert response.status_code == 200 assert b"Use a Passkey as a Second Factor" in response.data # we should have a wan key available assert b'action="/wan-signin' in response.data # verify NOT logged in response = client.get("/profile", follow_redirects=False) assert "/login" in response.location signin_options, response_url = _signin_start(client, "matt@lp.com") assert len(signin_options["allowCredentials"]) == 1 assert signin_options["allowCredentials"][0]["id"] == keys["secondary"]["id"] response = client.post( response_url, json=dict(credential=json.dumps(keys["secondary"]["signin"])), ) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.two_factor() @pytest.mark.unified_signin() def test_tf_json(app, client, get_message): # Test using webauthn key as a second factor # Register 2 keys - one "first" one "secondary" keys = reg_2_keys(client) logout(client) # log back in - should require MFA. response = client.post( "/us-signin", json=dict(identity="matt@lp.com", passcode="password", remember=True), ) assert response.status_code == 200 assert response.json["response"]["tf_method"] == "webauthn" assert response.json["response"]["tf_required"] assert response.json["response"]["tf_state"] == "ready" assert response.json["response"]["tf_signin_url"] == "/wan-signin" # verify NOT logged in response = client.get("/profile", headers={"accept": "application/json"}) assert response.status_code == 401 # For secondary, identity is stored in session signin_options, response_url, _ = _signin_start_json(client, "") assert len(signin_options["allowCredentials"]) == 1 assert signin_options["allowCredentials"][0]["id"] == keys["secondary"]["id"] response = client.post( response_url, json=dict(credential=json.dumps(keys["secondary"]["signin"])), ) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", headers={"accept": "application/json"}) assert response.status_code == 200 @pytest.mark.two_factor() @pytest.mark.settings(two_factor_always_validate=False) def test_tf_validity_window(app, client, get_message): # Test with a two-factor validity setting - we don't get re-prompted. authenticate(client) assert not client.get_cookie("tf_validity") register_options, response_url = _register_start_json(client) client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) logout(client) # login - should require second factor response = client.post( "/login", data=dict(email="matt@lp.com", password="password"), follow_redirects=True, ) assert b"Use a Passkey as a Second Factor" in response.data with client.session_transaction() as session: assert "tf_user_id" in session signin_options, response_url = _signin_start(client, "matt@lp.com") response = client.post(response_url, json=dict(credential=json.dumps(SIGNIN_DATA1))) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 reset_signcount(app, "matt@lp.com", "testr1") logout(client) # since we didn't specify 'remember' previously - should still require 2FA response = client.post( "/login", data=dict(email="matt@lp.com", password="password", remember=True), follow_redirects=True, ) assert b"Use a Passkey as a Second Factor" in response.data signin_options, response_url = _signin_start(client, "matt@lp.com") response = client.post(response_url, json=dict(credential=json.dumps(SIGNIN_DATA1))) assert response.status_code == 200 assert client.get_cookie("tf_validity") logout(client) # since we did specify 'remember' previously - should not require 2FA response = client.post( "/login", data=dict(email="matt@lp.com", password="password"), follow_redirects=True, ) # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 # Since logged in all tf related attributes in session should be gone. assert not tf_in_session(get_existing_session(client)) @pytest.mark.two_factor() @pytest.mark.settings(two_factor_always_validate=False) def test_tf_validity_window_json(app, client, get_message): # Test with a two-factor validity setting - we don't get re-prompted. # This also relies on the tf_validity_cookie json_authenticate(client) register_options, response_url = _register_start_json(client) client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) logout(client) response = client.post( "/login", json=dict(email="matt@lp.com", password="password", remember=True) ) assert response.status_code == 200 assert response.json["response"]["tf_required"] signin_options, response_url = _signin_start(client, "matt@lp.com") response = client.post(response_url, json=dict(credential=json.dumps(SIGNIN_DATA1))) assert response.status_code == 200 logout(client) # Sign in again - shouldn't require 2FA response = client.post( "/login", json=dict( email="matt@lp.com", password="password", remember=True, ), ) assert response.status_code == 200 response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.settings(wan_register_within=timedelta(seconds=1)) def test_register_timeout(app, client, get_message): authenticate(client) app.security.wan_serializer = FakeSerializer(1.0) register_options, response_url = _register_start_json(client, name="testr3") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_EXPIRED", within="1 second" ) @pytest.mark.settings(wan_signin_within=timedelta(seconds=2)) def test_signin_timeout(app, client, get_message): authenticate(client) register_options, response_url = _register_start_json(client, name="testr3") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 logout(client) app.security.wan_serializer = FakeSerializer(2.0) signin_options, response_url, _ = _signin_start_json(client, "matt@lp.com") response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA1)), ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_EXPIRED", within="2 seconds" ) def test_bad_token(app, client, get_message): authenticate(client) response = client.post("/wan-register/not a token", json=dict()) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) # same w/o json response = client.post( "/wan-register/not a token", data=dict(), follow_redirects=True ) assert get_message("API_ERROR") in response.data response = client.post( "/wan-register/not a token", data=dict(), follow_redirects=False ) assert "/wan-register" in response.location # Test wan-verify response = client.post("/wan-verify/not a token", json=dict()) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) # same w/o json response = client.post( "/wan-verify/not a token", data=dict(), follow_redirects=True ) assert get_message("API_ERROR") in response.data response = client.post( "/wan-verify/not a token", data=dict(), follow_redirects=False ) assert "/wan-verify" in response.location # Test signin logout(client) response = client.post("/wan-signin/not a token", json=dict()) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "API_ERROR" ) # same w/o json response = client.post( "/wan-signin/not a token", data=dict(), follow_redirects=True ) assert get_message("API_ERROR") in response.data response = client.post( "/wan-signin/not a token", data=dict(), follow_redirects=False ) assert "/wan-signin" in response.location @pytest.mark.settings( wan_register_template="custom_security/wan_register.html", wan_signin_template="custom_security/wan_signin.html", wan_verify_template="custom_security/wan_verify.html", ) def test_wan_context_processors(client, app): @app.security.context_processor def default_ctx_processor(): return {"global": "global"} @app.security.wan_register_context_processor def register_ctx(): return {"foo": "register"} authenticate(client) response = client.get("wan-register") assert b"CUSTOM WAN REGISTER" in response.data assert b"global" in response.data assert b"register" in response.data response = client.post("wan-register", data=dict(name="matt@lp.com")) assert b"CUSTOM WAN REGISTER" in response.data assert b"global" in response.data assert b"register" in response.data logout(client) @app.security.wan_signin_context_processor def signin_ctx(): return {"foo": "signin"} response = client.get("wan-signin") assert b"CUSTOM WAN SIGNIN" in response.data assert b"global" in response.data assert b"signin" in response.data response = client.post("wan-signin", data=dict(name="matt@lp.com")) assert b"CUSTOM WAN SIGNIN" in response.data assert b"global" in response.data assert b"signin" in response.data @app.security.wan_verify_context_processor def verify_ctx(): return {"foo": "verify"} authenticate(client) response = client.get("wan-verify") assert b"CUSTOM WAN VERIFY" in response.data assert b"global" in response.data assert b"verify" in response.data response = client.post("wan-verify", data=dict(name="matt@lp.com")) assert b"CUSTOM WAN VERIFY" in response.data assert b"global" in response.data assert b"verify" in response.data @pytest.mark.two_factor() def test_alt_tf(app, client, get_message): # Use webauthn as primary and set up SMS as second factor authenticate(client) register_options, response_url = _register_start_json(client, usage="first") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 sms_sender = setup_tf_sms(client) logout(client) # sign in using webauthn key signin_options, response_url, _ = _signin_start_json(client, "matt@lp.com") response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA1)), ) assert response.status_code == 200 assert response.json["response"]["tf_required"] code = sms_sender.messages[0].split()[-1] response = client.post("/tf-validate", json=dict(code=code)) assert response.status_code == 200 # verify logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.two_factor() def test_all_in_one(app, client, get_message): # Use a key that supports user_verification - we should be able to # use that alone. authenticate(client) register_options, response_url = _register_start_json(client, usage="first") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA_UV))) assert response.status_code == 200 setup_tf_sms(client) logout(client) signin_options, response_url, rjson = _signin_start_json(client, "matt@lp.com") assert "user" not in rjson["response"] response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA_UV)) ) assert response.json["response"]["user"]["email"] == "matt@lp.com" # verify actually logged in response = client.get("/profile", headers={"accept": "application/json"}) assert response.status_code == 200 @pytest.mark.two_factor() def test_all_in_one_not_allowed(app, client, get_message): # now test when we don't allow a key to satisfy both factors authenticate(client) register_options, response_url = _register_start_json(client, usage="first") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA_UV))) assert response.status_code == 200 setup_tf_sms(client) logout(client) app.config["SECURITY_WAN_ALLOW_AS_MULTI_FACTOR"] = False signin_options, response_url, rjson = _signin_start_json(client, "matt@lp.com") assert "user" not in rjson["response"] response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA_UV)) ) assert response.json["response"]["tf_required"] def test_reset(app, client): headers = {"Accept": "application/json", "Content-Type": "application/json"} authenticate(client) register_options, response_url = _register_start_json(client) client.post(response_url, json=dict(credential=json.dumps(REG_DATA_UV))) response = client.get("/wan-register", headers=headers) active_creds = response.json["response"]["registered_credentials"] assert active_creds[0]["name"] == "testr1" with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") app.security.datastore.webauthn_reset(user) app.security.datastore.commit() response = client.get("/wan-register", headers=headers) active_creds = response.json["response"]["registered_credentials"] assert len(active_creds) == 0 def test_user_handle(app, clients, get_message): """Test that we fail signin if user_handle doesn't match. Since we generated the SIGNIN_DATA_OH from view_scaffold - the user_handle has no way of matching. """ authenticate(clients) register_options, response_url = _register_start_json(clients, usage="first") response = clients.post(response_url, json=dict(credential=json.dumps(REG_DATA_UH))) assert response.status_code == 200 # verify can't sign in logout(clients) signin_options, response_url, _ = _signin_start_json(clients, "matt@lp.com") response = clients.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA_UH)) ) assert response.json["response"]["field_errors"]["credential"][0].encode( "utf-8" ) == get_message("WEBAUTHN_MISMATCH_USER_HANDLE") # Now change the user_handle both for the user and SIGNIN_DATA_UH with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") app.security.datastore.set_webauthn_user_handle(user) app.security.datastore.commit() b64_user_handle = urlsafe_b64encode( user.fs_webauthn_user_handle.encode() ).decode("utf-8") upd_signin_data = copy.deepcopy(SIGNIN_DATA_UH) upd_signin_data["response"]["userHandle"] = b64_user_handle signin_options, response_url, _ = _signin_start_json(clients, "matt@lp.com") response = clients.post( response_url, json=dict(credential=json.dumps(upd_signin_data)) ) # verify actually logged in response = clients.get("/profile", headers={"accept": "application/json"}) assert response.status_code == 200 def test_autogen_user_handle(app, client, get_message): # Test that is an existing user doesn't have a fs_webauthn_user_handle - it will # be generated. with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") user.fs_webauthn_user_handle = None app.security.datastore.put(user) app.security.datastore.commit() authenticate(client) register_options, response_url = _register_start_json(client, usage="first") with app.test_request_context("/"): user = app.security.datastore.find_user(email="matt@lp.com") assert user.fs_webauthn_user_handle b64_user_handle = ( urlsafe_b64encode(user.fs_webauthn_user_handle.encode()) .decode("utf-8") .replace("=", "") ) assert b64_user_handle == register_options["user"]["id"] def test_verify_json(app, client, get_message): # Test can re-authenticate using existing webauthn key. headers = {"Accept": "application/json", "Content-Type": "application/json"} authenticate(client) register_options, response_url = _register_start_json(client, usage="first") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 reset_fresh(client, app.config["SECURITY_FRESHNESS"]) response = client.get("fresh", headers=headers) assert response.status_code == 401 assert response.json["response"]["reauth_required"] assert response.json["response"]["has_webauthn_verify_credential"] response = client.get("wan-verify", headers=headers) assert response.status_code == 200 response = client.post("wan-verify", json=dict()) # default webauthn_utils will set userVerification to discouraged in the # case of verify. signin_options = response.json["response"]["credential_options"] assert signin_options["userVerification"] == "discouraged" response_url = ( f'wan-verify/{response.json["response"]["wan_state"]}?include_auth_token' ) response = client.post(response_url, json=dict(credential=json.dumps(SIGNIN_DATA1))) assert response.status_code == 200 verify_token(client, response.json["response"]["user"]["authentication_token"]) response = client.get("fresh", headers=headers) assert response.status_code == 200 def test_verify(app, client, get_message): # Test can re-authenticate using existing webauthn key. # Forms version - verify that the 'next' qparam is properly maintained during the # 2 part authentication. authenticate(client) register_options, response_url = _register_start(client, usage="first") response = client.post( response_url, data=dict(credential=json.dumps(REG_DATA1)), follow_redirects=True ) assert response.status_code == 200 assert b"testr1" in response.data old_paa = reset_fresh(client, app.config["SECURITY_FRESHNESS"]) response = client.get("fresh") assert response.location == "/verify?next=/fresh" signin_options, response_url = _signin_start( client, endpoint="wan-verify?next=/fresh" ) response = client.post( response_url, data=dict(credential=json.dumps(SIGNIN_DATA1)), follow_redirects=False, ) assert "/fresh" in response.location with client.session_transaction() as sess: assert sess["fs_paa"] > old_paa @pytest.mark.settings(wan_signin_within=timedelta(seconds=2)) def test_verify_timeout(app, client, get_message): authenticate(client) register_options, response_url = _register_start_json(client, name="testr3") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 app.security.wan_serializer = FakeSerializer(2.0) response = client.post("wan-verify", json=dict()) response_url = f'wan-verify/{response.json["response"]["wan_state"]}' response = client.post(response_url, json=dict(credential=json.dumps(SIGNIN_DATA1))) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_EXPIRED", within="2 seconds" ) def test_verify_validate_error(app, client, get_message, signals): authenticate(client) register_options, response_url = _register_start_json(client, name="testr3") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 response = client.post("wan-verify", json=dict()) response_url = f'wan-verify/{response.json["response"]["wan_state"]}' # send wrong signin data response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA_UH)) ) assert response.status_code == 400 assert response.json["response"]["field_errors"]["credential"][0].encode( "utf-8" ) == get_message("WEBAUTHN_UNKNOWN_CREDENTIAL_ID") # same thing - with forms - this should redirect to wan-verify and flash a message with capture_flashes() as flashes: response = client.post( response_url, data=dict(credential=json.dumps(SIGNIN_DATA_UH)), follow_redirects=False, ) assert response.status_code in [302, 303] assert "/wan-verify" in response.location assert flashes[0]["category"] == "error" assert flashes[0]["message"].encode("utf-8") == get_message( "WEBAUTHN_UNKNOWN_CREDENTIAL_ID" ) # now muck with attestation - should get VERIFY ERROR bad_signin = copy.deepcopy(SIGNIN_DATA1) bad_signin["response"]["signature"] = bad_signin["response"]["signature"].replace( "M", "N" ) response = client.post(response_url, json=dict(credential=json.dumps(bad_signin))) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_NO_VERIFY", cause="Could not verify authentication signature" ) assert ( signals["user_failed_authn"][0]["request_endpoint"] == "security.wan_verify_response" ) assert signals["user_failed_authn"][0]["user"].email == "matt@lp.com" assert signals["user_failed_authn"][0]["auth_type"] == "passkey" assert not signals["user_failed_authn"][0]["tfa"] check_signals( signals, ["user_failed_authn", "wan_registered", "user_authenticated"] ) def test_verify_rejects_cross_user_assertion(app, client, get_message): # The reauthentication-freshness gate must not be satisfied by a # WebAuthn assertion whose proven credential belongs to a different # user than the currently authenticated session user. Sibling of the # OAuth fix in `oauth_glue.oauth_verify_response`. authenticate(client, email="joe@lp.com") register_options, response_url = _register_start_json( client, name="joekey", usage="first" ) response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 logout(client) authenticate(client, email="matt@lp.com") register_options, response_url = _register_start_json( client, name="mattkey", usage="first" ) response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA_UV))) assert response.status_code == 200 old_paa = reset_fresh(client, app.config["SECURITY_FRESHNESS"]) response = client.post("wan-verify", json=dict()) response_url = f'wan-verify/{response.json["response"]["wan_state"]}' # Submit joe's assertion while logged in as matt. response = client.post(response_url, json=dict(credential=json.dumps(SIGNIN_DATA1))) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_MISMATCH_USER_HANDLE" ) with client.session_transaction() as sess: assert sess["fs_paa"] == old_paa # same with forms response = client.post( response_url, data=dict(credential=json.dumps(SIGNIN_DATA1)), follow_redirects=True, ) assert b"Reauthenticate" in response.data assert b"Credential user handle" in response.data @pytest.mark.settings(wan_allow_as_verify=None) def test_no_verify(app, client): authenticate(client) response = client.get("/wan-verify") assert response.status_code == 404 def test_verify_usage_any_json(app, client, get_message): # Test the WAN_ALLOW_AS_VERIFY config. # Make sure only allowed credentials show up as options # Make sure if we use a disallowed credential, we get an error. keys = reg_2_keys(client) # Default WAN_ALLOW_AS_VERIFY is ["first", "secondary"] response = client.post("wan-verify", json=dict()) response_url = f'wan-verify/{response.json["response"]["wan_state"]}' allow_credentials = response.json["response"]["credential_options"][ "allowCredentials" ] assert len(allow_credentials) == 2 # make sure can sign in with either response = client.post( response_url, json=dict(credential=json.dumps(keys["first"]["signin"])) ) assert response.status_code == 200 response = client.post( response_url, json=dict(credential=json.dumps(keys["secondary"]["signin"])) ) assert response.status_code == 200 @pytest.mark.settings(wan_allow_as_verify="first") def test_verify_usage_first_json(app, client, get_message): # Test the WAN_ALLOW_AS_VERIFY config. # Make sure only allowed credentials show up as options # Make sure if we use a disallowed credential, we get an error. keys = reg_2_keys(client) response = client.post("wan-verify", json=dict()) response_url = f'wan-verify/{response.json["response"]["wan_state"]}' allow_credentials = response.json["response"]["credential_options"][ "allowCredentials" ] assert len(allow_credentials) == 1 assert allow_credentials[0]["id"] == keys["first"]["id"] # make sure can sign in with just "first" response = client.post( response_url, json=dict(credential=json.dumps(keys["first"]["signin"])) ) assert response.status_code == 200 # but not "secondary" response = client.post( response_url, json=dict(credential=json.dumps(keys["secondary"]["signin"])) ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_CREDENTIAL_WRONG_USAGE" ) @pytest.mark.settings(wan_allow_as_verify="secondary") def test_verify_usage_secondary_json(app, client, get_message): # Test the WAN_ALLOW_AS_VERIFY config. # Make sure only allowed credentials show up as options # Make sure if we use a disallowed credential, we get an error. keys = reg_2_keys(client) response = client.post("wan-verify", json=dict()) response_url = f'wan-verify/{response.json["response"]["wan_state"]}' allow_credentials = response.json["response"]["credential_options"][ "allowCredentials" ] assert len(allow_credentials) == 1 assert allow_credentials[0]["id"] == keys["secondary"]["id"] # make sure can sign in with just "secondary" response = client.post( response_url, json=dict(credential=json.dumps(keys["first"]["signin"])) ) assert response.status_code == 400 assert response.json["response"]["errors"][0].encode("utf-8") == get_message( "WEBAUTHN_CREDENTIAL_WRONG_USAGE" ) response = client.post( response_url, json=dict(credential=json.dumps(keys["secondary"]["signin"])) ) assert response.status_code == 200 def test_remember_token(client): # test that remember token properly set on primary authn with webauthn authenticate(client) register_options, response_url = _register_start_json( client, name="testr3", usage="first" ) response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 logout(client) assert not client.get_cookie("remember_token") headers = {"Accept": "application/json", "Content-Type": "application/json"} response = client.post( "wan-signin", headers=headers, json=dict(identity="matt@lp.com", remember=True) ) response_url = f'wan-signin/{response.json["response"]["wan_state"]}' assert response.json["response"]["remember"] response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA1), remember=True), ) assert client.get_cookie("remember_token") client.delete_cookie("session") response = client.get("/profile") assert b"Profile Page" in response.data @pytest.mark.two_factor() @pytest.mark.unified_signin() def test_remember_token_tf(client): # test that remember token properly set after secondary authn with webauthn authenticate(client) register_options, response_url = _register_start_json(client, name="testr3") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 logout(client) assert not client.get_cookie("remember_token") # login again - should require MFA response = client.post( "/us-signin", json=dict(identity="matt@lp.com", passcode="password", remember=True), ) assert response.status_code == 200 assert response.json["response"]["tf_method"] == "webauthn" assert response.json["response"]["tf_required"] with client.session_transaction() as session: assert session["tf_remember_login"] signin_options, response_url, _ = _signin_start_json(client, "matt@lp.com") response = client.post( response_url, json=dict(credential=json.dumps(SIGNIN_DATA1), remember=True), ) assert client.get_cookie("remember_token") client.delete_cookie("session") response = client.get("/profile") assert b"profile" in response.data @pytest.mark.settings( wan_post_register_view="/post_register", ) def test_post_register_redirect(app, client, get_message): authenticate(client) register_options, response_url = _register_start_json(client, name="testr3") response = client.post( response_url, data=dict(credential=json.dumps(REG_DATA1)), follow_redirects=False, ) assert response.status_code in [302, 303] assert "/post_register" in response.location class MyWebauthnUtil(HackWebauthnUtil): def user_verification(self, user, usage): from webauthn.helpers.structs import UserVerificationRequirement return UserVerificationRequirement.REQUIRED @pytest.mark.two_factor() @pytest.mark.unified_signin() @pytest.mark.webauthn(webauthn_util_cls=MyWebauthnUtil) def test_uv_required(client): # Override WebauthnUtils to require user-verification on signin. keys = reg_2_keys(client) logout(client) # log back in - should require MFA. response = client.post( "/us-signin", json=dict(identity="matt@lp.com", passcode="password", remember=True), ) assert response.status_code == 200 assert response.json["response"]["tf_required"] # since we always REQUIRE user_verification in our WebauthUtil this should fail signin_options, response_url, _ = _signin_start_json(client, "") response = client.post( response_url, json=dict(credential=json.dumps(keys["secondary"]["signin"])), ) assert response.status_code == 400 assert ( "User verification is required" in response.json["response"]["field_errors"]["credential"][0] ) logout(client) # Try signing in with 'first' WebAuthn key - this DOES have UV set so should work. signin_options, response_url, _ = _signin_start_json(client, "") response = client.post( response_url, json=dict(credential=json.dumps(keys["first"]["signin"])), ) assert response.status_code == 200 assert response.json["response"]["user"]["email"] == "matt@lp.com" @pytest.mark.settings(multi_factor_recovery_codes=True) def test_mf(client): # Test using recovery codes in-liu of a webauthn second factor # Note that we are allowed to generate recovery codes even if we don't yet have # an established 2nd factor headers = {"Accept": "application/json", "Content-Type": "application/json"} authenticate(client) response = client.post("/mf-recovery-codes", headers=headers) codes = response.json["response"]["recovery_codes"] assert len(codes) == 5 # setup webauthn register_options, response_url = _register_start_json(client, name="testr3") response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 logout(client) response = client.post( "/login", json=dict(email="matt@lp.com", password="password") ) assert response.status_code == 200 assert response.json["response"]["tf_required"] # oh no - we forgot our webauthn key """ Right now tf-rescue is part of TWO_FACTOR - not WEBAUTHN response = client.get("/tf-rescue", headers=headers) options = response.json["response"]["recovery_options"] assert "recovery_code" in options.keys() assert "/mf-recovery" in options["recovery_code"] """ response = client.post( "/mf-recovery", data=dict(code=codes[0]), follow_redirects=True ) assert response.status_code == 200 # verify actually logged in response = client.get("/profile", follow_redirects=False) assert response.status_code == 200 @pytest.mark.settings(url_prefix="/auth") def test_login_next(app, client, get_message): # Test that ?next=/xx is propagated through login/wan-signin templates as well as # views. # Also - use a different blueprint prefix - we rarely test that.... authenticate(client, endpoint="/auth/login") register_options, response_url = _register_start( client, name="testr3", usage="first", endpoint="/auth/wan-register" ) response = client.post( response_url, data=dict(credential=json.dumps(REG_DATA1)), follow_redirects=True ) assert response.status_code == 200 assert get_message("WEBAUTHN_REGISTER_SUCCESSFUL", name="testr3") in response.data logout(client, endpoint="/auth/logout") response = client.get("profile", follow_redirects=True) assert "?next=/profile" in response.request.url # pull webauthn form action out of login_form - should have ?next=... webauthn_url = get_form_action(response, 1) signin_options, response_url = _signin_start( client, "matt@lp.com", endpoint=webauthn_url ) response = client.post( response_url, data=dict(credential=json.dumps(SIGNIN_DATA1)), follow_redirects=True, ) assert response.status_code == 200 assert b"Profile Page" in response.data # Try form.next logout(client, endpoint="/auth/logout") reset_signcount(app, "matt@lp.com", "testr3") response = client.post( "/auth/wan-signin", data=dict(identity="matt@lp.com", next="/im-in") ) response_url = get_form_action(response) next_loc = get_form_input_value(response, "next") response = client.post( response_url, data=dict(credential=json.dumps(SIGNIN_DATA1), next=next_loc), follow_redirects=False, ) assert "/im-in" in response.location @pytest.mark.flask_async() def test_async(app, client, get_message): auths = [] @user_authenticated.connect_via(app) async def authned(myapp, user, **extra_args): auths.append((user.email, extra_args["authn_via"])) @wan_registered.connect_via(app) async def pc(sender, user, name, **extra_args): assert name == "testr1" assert len(user.webauthn) == 1 @wan_deleted.connect_via(app) async def wan_delete(sender, user, name, **extra_args): assert name == "testr1" authenticate(client) register_options, response_url = _register_start(client, usage="first") response = client.post( response_url, data=dict(credential=json.dumps(REG_DATA1)), follow_redirects=True ) assert response.status_code == 200 # sign in - simple case use identity so we get back allowCredentials logout(client) signin_options, response_url = _signin_start(client, "matt@lp.com") response = client.post( response_url, data=dict(credential=json.dumps(SIGNIN_DATA1)), follow_redirects=True, ) assert response.status_code == 200 assert b"Welcome matt@lp.com" in response.data assert len(auths) == 2 assert auths[1][1] == ["webauthn"] # test delete signal response = client.post( "/wan-delete", data=dict(name="testr1"), follow_redirects=True ) @pytest.mark.csrf() @pytest.mark.settings( wan_post_register_view="/done-registration", post_login_view="/post-login", ) def test_csrf(app, client, get_message): response = client.get("/login") csrf_token = get_form_input_value(response, "csrf_token") authenticate(client, csrf=True) register_options, response_url = _register_start( client, usage="first", csrf_token=csrf_token ) data = dict(credential=json.dumps(REG_DATA1)) response = client.post(response_url, data=data, follow_redirects=True) assert ( b"The CSRF token is missing" in response.data ) # this should have been flashed data["csrf_token"] = csrf_token response = client.post(response_url, data=data) assert check_location(app, response.location, "/done-registration") logout(client) # use old csrf_token - should fail and we should get the error in the template response = client.post( "wan-signin", data=dict(identity="matt@lp.com", csrf_token=csrf_token) ) assert b"The CSRF tokens do not match." in response.data response = client.get("/wan-signin") csrf_token = get_form_input_value(response, "csrf_token") signin_options, response_url = _signin_start( client, "matt@lp.com", csrf_token=csrf_token ) data = dict(credential=json.dumps(SIGNIN_DATA1)) response = client.post(response_url, data=data, follow_redirects=True) assert ( b"The CSRF token is missing" in response.data ) # this should have been flashed data["csrf_token"] = csrf_token response = client.post(response_url, data=data) assert check_location(app, response.location, "/post-login") @pytest.mark.settings(default_remember_me=True) def test_remember_login_form(app, client): # ensure form has the checkbox set if default set. response = client.get("/wan-signin") assert response.status_code == 200 r = get_form_input(response, "remember") assert "checked" in r def test_name_clean(app, client, get_message): # Ensure name is sanitized, normalized, checked for length authenticate(client) name = "hi " response = client.post("wan-register", json=dict(name=name, usage="secondary")) assert response.status_code == 400 assert ( get_message("INVALID_INPUT") == response.json["response"]["field_errors"]["name"][0].encode() ) name = "hi there?" response = client.post("wan-register", json=dict(name=name, usage="secondary")) assert response.status_code == 400 assert ( get_message("WEBAUTHN_NAME_DISALLOWED_CHARACTERS") == response.json["response"]["field_errors"]["name"][0].encode() ) name = "howlongcanIbebeforeyoucomplainIsupposereallyreallylongandlongerreally" response = client.post("wan-register", json=dict(name=name, usage="secondary")) assert response.status_code == 400 assert ( get_message("INVALID_INPUT_LENGTH", length=64) == response.json["response"]["field_errors"]["name"][0].encode() ) # ensure delete also sanitizes name = "hi " response = client.post("/wan-delete", json=dict(name=name)) assert response.status_code == 400 assert ( get_message("INVALID_INPUT") == response.json["response"]["field_errors"]["name"][0].encode() ) def test_name_normalize(app, client, get_message): headers = {"Accept": "application/json", "Content-Type": "application/json"} name = "Imnumber\N{ROMAN NUMERAL ONE}" authenticate(client) register_options, response_url = _register_start_json(client, name=name) response = client.post(response_url, json=dict(credential=json.dumps(REG_DATA1))) assert response.status_code == 200 response = client.get("/wan-register", headers=headers) active_creds = response.json["response"]["registered_credentials"] assert active_creds[0]["name"] == "ImnumberI" alt_name = "Imnumber\N{LATIN CAPITAL LETTER I}" response = client.post("/wan-delete", json=dict(name=alt_name)) assert response.status_code == 200 pallets-eco-flask-security-72bf929/tests/translations/000077500000000000000000000000001525721743500231315ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/tests/translations/cic_US/000077500000000000000000000000001525721743500242765ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/tests/translations/cic_US/LC_MESSAGES/000077500000000000000000000000001525721743500260635ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/tests/translations/cic_US/LC_MESSAGES/flask_security.po000066400000000000000000001014731525721743500314600ustar00rootroot00000000000000# msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "POT-Creation-Date: 2026-09-24 09:44-0700\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language: cic_US\n" "Language-Team: cic_US \n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.18.0\n" #: flask_security/core.py:292 msgid "Confirm your new email address" msgstr "" #: flask_security/core.py:363 msgid "Login Required" msgstr "" #: flask_security/core.py:364 msgid "Welcome" msgstr "Welcome OKAY!" #: flask_security/core.py:365 msgid "Please confirm your email" msgstr "" #: flask_security/core.py:366 msgid "Login instructions" msgstr "" #: flask_security/core.py:367 msgid "Your password has been reset" msgstr "" #: flask_security/core.py:368 msgid "Your password has been changed" msgstr "Your password has been changed OKAY!" #: flask_security/core.py:369 msgid "Password reset instructions" msgstr "" #: flask_security/core.py:370 msgid "Your username has been changed" msgstr "Your username has been changed OKAY!" #: flask_security/core.py:371 msgid "Your requested username" msgstr "" #: flask_security/core.py:374 msgid "Two-Factor Login" msgstr "" #: flask_security/core.py:375 msgid "Two-Factor Rescue" msgstr "" #: flask_security/core.py:423 msgid "Verification Code" msgstr "" #: flask_security/core.py:472 msgid "Input not appropriate for requested API" msgstr "Input not appropriate for requested API OKAY!" #: flask_security/core.py:474 msgid "Authentication failed - identity or password/passcode invalid" msgstr "" #: flask_security/core.py:479 msgid "" "If that email address is in our system, you will receive an email " "describing how to reset your password." msgstr "" #: flask_security/core.py:485 msgid "If that identity is in our system, you were sent a code." msgstr "" #: flask_security/core.py:488 msgid "You do not have permission to view this resource." msgstr "" #: flask_security/core.py:490 msgid "You must sign in to view this resource." msgstr "" #: flask_security/core.py:494 msgid "You must reauthenticate to access this endpoint" msgstr "" #: flask_security/core.py:499 #, python-format msgid "" "Thank you. To confirm your email address %(email)s, please click on the " "link in the email we have just sent to you." msgstr "Thanks %(email)s. OKAY!" #: flask_security/core.py:505 msgid "Thank you. Your email has been confirmed." msgstr "" #: flask_security/core.py:506 msgid "Your email has already been confirmed." msgstr "" #: flask_security/core.py:507 msgid "Invalid confirmation token." msgstr "" #: flask_security/core.py:509 #, python-format msgid "%(email)s is already associated with an account." msgstr "" #: flask_security/core.py:514 #, python-format msgid "" "Identity attribute '%(attr)s' with value '%(value)s' is already " "associated with an account." msgstr "" #: flask_security/core.py:520 #, python-format msgid "Identity %(id)s not registered" msgstr "" #: flask_security/core.py:525 #, python-format msgid "" "An error occurred while communicating with the OAuth provider: " "(%(exerror)s - %(exdesc)s). Please try again." msgstr "" #: flask_security/core.py:531 msgid "Password does not match" msgstr "" #: flask_security/core.py:532 msgid "Passwords do not match" msgstr "" #: flask_security/core.py:533 msgid "Redirections outside the domain are forbidden" msgstr "" #: flask_security/core.py:534 msgid "Recovery code invalid" msgstr "" #: flask_security/core.py:535 msgid "No recovery codes generated yet" msgstr "" #: flask_security/core.py:537 #, python-format msgid "Instructions to reset your password have been sent to %(email)s." msgstr "" #: flask_security/core.py:541 #, python-format msgid "You did not reset your password within %(within)s. " msgstr "" #: flask_security/core.py:544 msgid "Invalid reset password token." msgstr "" #: flask_security/core.py:545 msgid "Email requires confirmation." msgstr "" #: flask_security/core.py:547 #, python-format msgid "Confirmation instructions have been sent to %(email)s." msgstr "" #: flask_security/core.py:551 #, python-format msgid "You did not confirm your email within %(within)s. " msgstr "" #: flask_security/core.py:556 #, python-format msgid "" "You did not login within %(within)s. New instructions to login have been " "sent to %(email)s." msgstr "" #: flask_security/core.py:562 #, python-format msgid "Instructions to login have been sent to %(email)s." msgstr "" #: flask_security/core.py:565 msgid "Invalid login token." msgstr "" #: flask_security/core.py:566 msgid "Account is disabled." msgstr "" # test_cli #: flask_security/core.py:567 msgid "Email not provided" msgstr "Email not provided OKAY!" #: flask_security/core.py:568 msgid "Invalid email address" msgstr "" #: flask_security/core.py:569 flask_security/core.py:624 msgid "Invalid code" msgstr "" #: flask_security/core.py:571 msgid "Value entered is not valid for the field" msgstr "" #: flask_security/core.py:575 #, python-format msgid "Value entered must be less than %(length) characters" msgstr "" #: flask_security/core.py:578 msgid "Password not provided" msgstr "Password not provided OKAY!" #: flask_security/core.py:580 #, python-format msgid "Password must be at least %(length)s characters" msgstr "" #: flask_security/core.py:583 msgid "Password not complex enough" msgstr "" #: flask_security/core.py:584 msgid "Password on breached list" msgstr "" #: flask_security/core.py:586 msgid "Failed to contact breached passwords site" msgstr "" #: flask_security/core.py:589 msgid "Phone number not valid e.g. missing country code" msgstr "" #: flask_security/core.py:590 msgid "Specified user does not exist" msgstr "" #: flask_security/core.py:591 msgid "Invalid password" msgstr "" #: flask_security/core.py:592 msgid "Password or code submitted is not valid" msgstr "" #: flask_security/core.py:593 msgid "You have successfully logged in." msgstr "" #: flask_security/core.py:594 flask_security/templates/security/_menu.html:21 #: flask_security/templates/security/_menu.html:67 msgid "Forgot password?" msgstr "" #: flask_security/core.py:597 msgid "" "You successfully reset your password and you have been logged in " "automatically." msgstr "" #: flask_security/core.py:604 msgid "" "You successfully reset your password. Please authenticate using your new " "password." msgstr "" #: flask_security/core.py:610 msgid "Your new password must be different than your previous password." msgstr "" #: flask_security/core.py:613 msgid "You successfully changed your password." msgstr "You successfully changed your password. OKAY!" #: flask_security/core.py:614 msgid "Please log in to access this page." msgstr "" #: flask_security/core.py:615 msgid "Please reauthenticate to access this page." msgstr "" #: flask_security/core.py:616 #, python-format msgid "Refresh token is invalid (%(reason)s)" msgstr "" #: flask_security/core.py:617 msgid "Reauthentication successful" msgstr "" #: flask_security/core.py:619 msgid "You can only access this endpoint when not logged in." msgstr "" #: flask_security/core.py:622 msgid "Code has been sent." msgstr "" #: flask_security/core.py:623 msgid "Failed to send code. Please try again later" msgstr "" #: flask_security/core.py:625 msgid "Your code has been confirmed" msgstr "" #: flask_security/core.py:627 msgid "You successfully changed your two-factor method." msgstr "" #: flask_security/core.py:631 msgid "You currently do not have permissions to access this page" msgstr "" #: flask_security/core.py:634 msgid "Marked method is not valid" msgstr "" #: flask_security/core.py:636 msgid "You successfully disabled two-factor authorization." msgstr "" #: flask_security/core.py:640 flask_security/core.py:649 #, python-format msgid "Setup must be completed within %(within)s. Please start over." msgstr "" # unified signin setup methods #: flask_security/core.py:644 #, python-format msgid "Currently active sign in options: %(method_list)s." msgstr "Currently active sign in options: %(method_list)s. OKAY!" #: flask_security/core.py:647 msgid "Requested method is not valid" msgstr "" #: flask_security/core.py:652 msgid "Unified sign in setup successful" msgstr "" #: flask_security/core.py:653 msgid "You must specify a valid identity to sign in" msgstr "" #: flask_security/core.py:654 #, python-format msgid "Use this code to sign in: %(code)s" msgstr "" #: flask_security/core.py:655 msgid "You successfully changed your username" msgstr "You successfully changed your username OKAY!" #: flask_security/core.py:658 #, python-format msgid "" "Username must be at least %(min)d characters and less than %(max)d " "characters" msgstr "" #: flask_security/core.py:664 msgid "Username can contain only letters and numbers" msgstr "" #: flask_security/core.py:667 msgid "Username not provided" msgstr "" #: flask_security/core.py:669 #, python-format msgid "%(username)s is already associated with an account." msgstr "" #: flask_security/core.py:673 #, python-format msgid "Passkey operations must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:677 msgid "Nickname for new passkey is required." msgstr "" #: flask_security/core.py:681 #, python-format msgid "%(name)s is already associated with a passkey." msgstr "" #: flask_security/core.py:685 #, python-format msgid "%(name)s not registered with current user." msgstr "" #: flask_security/core.py:690 msgid "" "Passkey nicknames can contain only letters, numbers, and limited " "punctuation." msgstr "" #: flask_security/core.py:696 #, python-format msgid "Successfully deleted the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:700 #, python-format msgid "Successfully added the passkey with name: %(name)s" msgstr "" #: flask_security/core.py:704 msgid "Passkey already registered." msgstr "" #: flask_security/core.py:708 msgid "Unregistered passkey." msgstr "" #: flask_security/core.py:712 msgid "Passkey doesn't belong to any user." msgstr "" #: flask_security/core.py:716 #, python-format msgid "Could not verify passkey: %(cause)s." msgstr "" #: flask_security/core.py:720 msgid "Passkey not registered for this use (first or secondary)" msgstr "" #: flask_security/core.py:724 msgid "Credential user handle didn't match" msgstr "" #: flask_security/core.py:728 #, python-format msgid "Confirmation must be completed within %(within)s. Please start over." msgstr "" #: flask_security/core.py:732 msgid "Change of email address confirmed" msgstr "" #: flask_security/core.py:737 #, python-format msgid "" "Instructions to confirm your new email address have been sent to " "%(email)s." msgstr "" #: flask_security/core.py:743 msgid "If registered, your username will be sent to your email." msgstr "" #: flask_security/forms.py:70 msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" msgstr "" #: flask_security/forms.py:72 msgid "Change Method" msgstr "" #: flask_security/forms.py:73 flask_security/templates/security/_menu.html:16 #: flask_security/templates/security/change_password.html:2 #: flask_security/templates/security/change_password.html:7 msgid "Change Password" msgstr "Change Password OKAY!" #: flask_security/forms.py:74 msgid "Authentication Code" msgstr "" #: flask_security/forms.py:75 msgid "Delete" msgstr "" #: flask_security/forms.py:76 msgid "Email Address" msgstr "Email Address OKAY!" #: flask_security/forms.py:77 msgid "Set up using email" msgstr "" #: flask_security/forms.py:78 msgid "Error(s)" msgstr "" #: flask_security/forms.py:79 msgid "Identity" msgstr "" #: flask_security/forms.py:80 flask_security/templates/security/_menu.html:52 #: flask_security/templates/security/login_user.html:2 #: flask_security/templates/security/login_user.html:7 #: flask_security/templates/security/send_login.html:2 #: flask_security/templates/security/send_login.html:7 msgid "Login" msgstr "Login OKAY!" #: flask_security/forms.py:81 msgid "New Password" msgstr "New Password OKAY!" #: flask_security/forms.py:82 msgid "Passcode" msgstr "Passcode OKAY!" #: flask_security/forms.py:83 msgid "Password" msgstr "Password OKAY!" #: flask_security/forms.py:84 msgid "Phone Number" msgstr "Phone Number OKAY!" #: flask_security/forms.py:85 msgid "Recover Password" msgstr "" #: flask_security/forms.py:86 flask_security/templates/security/_menu.html:72 msgid "Recover Username" msgstr "" #: flask_security/forms.py:87 flask_security/templates/security/_menu.html:62 #: flask_security/templates/security/register_user.html:2 #: flask_security/templates/security/register_user.html:7 msgid "Register" msgstr "Register OKAY!" #: flask_security/forms.py:88 msgid "Remember Me" msgstr "Remember Me OKAY!" #: flask_security/forms.py:89 msgid "Reset Password" msgstr "" #: flask_security/forms.py:90 msgid "Retype Password" msgstr "Retype Password OKAY!" #: flask_security/forms.py:91 msgid "Resend Confirmation Instructions" msgstr "" #: flask_security/forms.py:92 msgid "Send Login Link" msgstr "" #: flask_security/forms.py:93 msgid "Send Code" msgstr "" #: flask_security/forms.py:94 #: flask_security/templates/security/us_signin.html:2 #: flask_security/templates/security/us_signin.html:7 msgid "Sign In" msgstr "" #: flask_security/forms.py:95 msgid "Set up using SMS" msgstr "" #: flask_security/forms.py:96 msgid "Submit" msgstr "Submit OKAY!" #: flask_security/forms.py:97 msgid "Submit Code" msgstr "" #: flask_security/forms.py:98 msgid "Username" msgstr "" #: flask_security/forms.py:99 msgid "Verify Password" msgstr "" #: flask_security/forms.py:104 msgid "Google Authenticator" msgstr "" #: flask_security/forms.py:105 msgid "authenticator" msgstr "" #: flask_security/forms.py:106 flask_security/forms.py:107 msgid "email" msgstr "email OKAY!" #: flask_security/forms.py:108 msgid "SMS" msgstr "" #: flask_security/forms.py:109 msgid "password" msgstr "password OKAY!" #: flask_security/forms.py:110 msgid "passkey" msgstr "" #: flask_security/forms.py:111 msgid "none" msgstr "none OKAY!" #: flask_security/forms.py:725 flask_security/tokens.py:201 msgid "Refresh Token" msgstr "" #: flask_security/forms.py:1089 flask_security/unified_signin.py:176 msgid "Available Methods" msgstr "" #: flask_security/forms.py:1091 msgid "Disable two-factor authentication" msgstr "" #: flask_security/forms.py:1189 msgid "Trouble Accessing Your Account?/Lost Mobile Device?" msgstr "" #: flask_security/forms.py:1191 msgid "Contact Administrator" msgstr "" #: flask_security/recovery_codes.py:144 msgid "Show Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:147 msgid "Generate New Recovery Codes" msgstr "" #: flask_security/recovery_codes.py:163 msgid "Recovery Code" msgstr "" #: flask_security/tf_plugin.py:52 msgid "Available Second Factor Methods:" msgstr "" #: flask_security/tf_plugin.py:54 msgid "Select" msgstr "" #: flask_security/twofactor.py:147 msgid "Send code via email" msgstr "" #: flask_security/twofactor.py:160 msgid "Use previously downloaded recovery code" msgstr "" #: flask_security/unified_signin.py:168 msgid "Code or Password" msgstr "" #: flask_security/unified_signin.py:178 msgid "Via email" msgstr "" #: flask_security/unified_signin.py:179 msgid "Via SMS" msgstr "" #: flask_security/unified_signin.py:318 msgid "Setup additional sign in option" msgstr "" #: flask_security/unified_signin.py:331 msgid "Delete active sign in option" msgstr "" #: flask_security/webauthn.py:127 flask_security/webauthn.py:387 msgid "Nickname" msgstr "" #: flask_security/webauthn.py:131 msgid "Usage" msgstr "" #: flask_security/webauthn.py:133 msgid "Use as a first authentication factor" msgstr "" #: flask_security/webauthn.py:136 msgid "Use as a secondary authentication factor" msgstr "" #: flask_security/webauthn.py:241 msgid "Start" msgstr "" #: flask_security/templates/security/_menu.html:5 msgid "Menu" msgstr "" #: flask_security/templates/security/_menu.html:26 msgid "Change Registered Email" msgstr "" #: flask_security/templates/security/_menu.html:31 #: flask_security/templates/security/change_username.html:2 #: flask_security/templates/security/change_username.html:7 msgid "Change Username" msgstr "Change Username OKAY!" #: flask_security/templates/security/_menu.html:36 #: flask_security/templates/security/two_factor_setup.html:22 msgid "Two-Factor Setup" msgstr "" #: flask_security/templates/security/_menu.html:41 msgid "Unified Signin Setup" msgstr "" #: flask_security/templates/security/_menu.html:46 msgid "Passkey Setup" msgstr "" #: flask_security/templates/security/_menu.html:57 msgid "Unified Sign In" msgstr "" #: flask_security/templates/security/_menu.html:77 msgid "Confirm account" msgstr "" #: flask_security/templates/security/change_email.html:2 #: flask_security/templates/security/change_email.html:7 msgid "Change Email" msgstr "" #: flask_security/templates/security/change_email.html:8 msgid "" "Once submitted, an email confirmation will be sent to this new email " "address." msgstr "" #: flask_security/templates/security/change_password.html:14 msgid "You do not currently have a password - this will add one." msgstr "" #: flask_security/templates/security/change_username.html:9 #, python-format msgid "Current username is: %(username)s" msgstr "" #: flask_security/templates/security/forgot_password.html:2 #: flask_security/templates/security/forgot_password.html:7 msgid "Send password reset instructions" msgstr "" #: flask_security/templates/security/login_user.html:14 msgid "or" msgstr "" #: flask_security/templates/security/login_user.html:24 #: flask_security/templates/security/us_signin.html:26 msgid "Use a Passkey to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:27 #: flask_security/templates/security/us_signin.html:29 msgid "Sign in with a passkey" msgstr "" #: flask_security/templates/security/login_user.html:33 #: flask_security/templates/security/us_signin.html:35 msgid "Use Social Login to Sign In" msgstr "" #: flask_security/templates/security/login_user.html:37 #: flask_security/templates/security/us_signin.html:39 #: flask_security/templates/security/us_verify.html:39 #: flask_security/templates/security/verify.html:28 #, python-format msgid "Continue with %(provider)s" msgstr "" #: flask_security/templates/security/logout_user.html:2 #: flask_security/templates/security/logout_user.html:7 msgid "Confirm Sign Out" msgstr "" #: flask_security/templates/security/mf_recovery.html:2 #: flask_security/templates/security/mf_recovery.html:7 msgid "Enter Recovery Code" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:2 #: flask_security/templates/security/mf_recovery_codes.html:7 #: flask_security/templates/security/two_factor_setup.html:81 #: flask_security/templates/security/wan_register.html:76 msgid "Recovery Codes" msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:13 msgid "" "Be sure to copy these and store in a safe place. Each code can be used " "only once." msgstr "" #: flask_security/templates/security/mf_recovery_codes.html:21 msgid "Generate new Recovery Codes" msgstr "" #: flask_security/templates/security/recover_username.html:2 #: flask_security/templates/security/recover_username.html:7 msgid "Username Recovery" msgstr "" #: flask_security/templates/security/reset_password.html:2 #: flask_security/templates/security/reset_password.html:7 msgid "Reset password" msgstr "" #: flask_security/templates/security/send_confirmation.html:2 #: flask_security/templates/security/send_confirmation.html:7 msgid "Resend confirmation instructions" msgstr "" #: flask_security/templates/security/two_factor_select.html:2 #: flask_security/templates/security/two_factor_select.html:7 msgid "Select Two-Factor Method" msgstr "" #: flask_security/templates/security/two_factor_setup.html:28 msgid "Two-Factor authentication adds an extra layer of security to your account" msgstr "" #: flask_security/templates/security/two_factor_setup.html:29 msgid "In addition to your username and password, you'll need to use a code." msgstr "" #: flask_security/templates/security/two_factor_setup.html:33 #, python-format msgid "Currently setup two-factor method: %(method)s" msgstr "Currently setup two-factor method: %(method)s OKAY!" #: flask_security/templates/security/two_factor_setup.html:52 #: flask_security/templates/security/us_setup.html:61 msgid "" "Open an authenticator app on your device and scan the following QRcode " "(or enter the code below manually) to start receiving codes:" msgstr "" #: flask_security/templates/security/two_factor_setup.html:55 msgid "Two-Factor authentication code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:66 msgid "Enter code to complete setup" msgstr "" #: flask_security/templates/security/two_factor_setup.html:75 #: flask_security/templates/security/two_factor_verify_code.html:11 msgid "enter numeric code" msgstr "" #: flask_security/templates/security/two_factor_setup.html:83 #: flask_security/templates/security/wan_register.html:78 msgid "This application supports setting up recovery codes." msgstr "" #: flask_security/templates/security/two_factor_setup.html:84 #: flask_security/templates/security/two_factor_setup.html:92 #: flask_security/templates/security/us_setup.html:90 #: flask_security/templates/security/wan_register.html:79 msgid "You can set them up here." msgstr "" #: flask_security/templates/security/two_factor_setup.html:89 msgid "Passkeys" msgstr "" #: flask_security/templates/security/two_factor_setup.html:91 #: flask_security/templates/security/us_setup.html:89 msgid "This application supports passkeys." msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:2 #: flask_security/templates/security/two_factor_verify_code.html:7 msgid "Two-Factor Authentication" msgstr "" # test_two_factor - two_factor_verify_code.html #: flask_security/templates/security/two_factor_verify_code.html:8 #, python-format msgid "Please enter your authentication code generated via: %(method)s" msgstr "Please enter your authentication code generated via: %(method)s OKAY!" #: flask_security/templates/security/two_factor_verify_code.html:21 msgid "The code for authentication was sent to your email address" msgstr "" #: flask_security/templates/security/two_factor_verify_code.html:24 msgid "An email was sent to us in order to reset your application account" msgstr "" #: flask_security/templates/security/us_setup.html:25 #: flask_security/templates/security/us_setup.html:30 msgid "Setup Unified Sign In" msgstr "" #: flask_security/templates/security/us_setup.html:64 msgid "Passwordless QRCode" msgstr "" #: flask_security/templates/security/us_setup.html:71 msgid "No methods have been enabled - nothing to setup" msgstr "" #: flask_security/templates/security/us_setup.html:77 msgid "Enter code here to complete setup" msgstr "" #: flask_security/templates/security/us_signin.html:16 #: flask_security/templates/security/us_verify.html:13 msgid "Request one-time code be sent" msgstr "" #: flask_security/templates/security/us_verify.html:2 #: flask_security/templates/security/us_verify.html:7 #: flask_security/templates/security/verify.html:2 #: flask_security/templates/security/verify.html:7 #: flask_security/templates/security/wan_verify.html:10 msgid "Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:18 msgid "Code has been sent" msgstr "" #: flask_security/templates/security/us_verify.html:26 #: flask_security/templates/security/verify.html:15 msgid "Use a Passkey to Reauthenticate" msgstr "" #: flask_security/templates/security/us_verify.html:35 #: flask_security/templates/security/verify.html:24 msgid "Use Social Login to Reauthenticate" msgstr "" #: flask_security/templates/security/wan_register.html:5 #: flask_security/templates/security/wan_register.html:16 msgid "Setup a New Passkey" msgstr "" #: flask_security/templates/security/wan_register.html:19 msgid "Start by providing a unique name for your passkey:" msgstr "" #: flask_security/templates/security/wan_register.html:54 msgid "Currently registered passkeys:" msgstr "" #: flask_security/templates/security/wan_register.html:55 #, python-format msgid "" "Nickname: \"%s\" Usage: \"%s\" Transports: \"%s\" Discoverable: \"%s\" " "Device Type: \"%s\" Backed up? \"%s\" Last used on: %s" msgstr "" #: flask_security/templates/security/wan_register.html:66 msgid "Delete an Existing Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:5 msgid "Sign In With A Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:17 msgid "Sign In With a Passkey" msgstr "" #: flask_security/templates/security/wan_signin.html:19 msgid "Use a Passkey as a Second Factor" msgstr "" #: flask_security/templates/security/wan_verify.html:21 msgid "Reauthenticate Using a Passkey" msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:9 #, python-format msgid "Use this link to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:10 #: flask_security/templates/security/email/change_email_instructions.txt:10 #: flask_security/templates/security/email/confirmation_instructions.html:10 #: flask_security/templates/security/email/confirmation_instructions.txt:10 #: flask_security/templates/security/email/reset_instructions.html:10 #: flask_security/templates/security/email/reset_instructions.txt:10 #, python-format msgid "This link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.html:11 #: flask_security/templates/security/email/change_email_instructions.txt:11 #, python-format msgid "Your currently registered email is %(email)s." msgstr "" #: flask_security/templates/security/email/change_email_instructions.txt:9 #, python-format msgid "Use %(link)s to confirm your new email address." msgstr "" #: flask_security/templates/security/email/change_notice.html:1 #: flask_security/templates/security/email/change_notice.txt:1 msgid "Your password has been changed." msgstr "Your password has been changed. OKAY!" #: flask_security/templates/security/email/change_notice.html:4 msgid "If you did not change your password," msgstr "" #: flask_security/templates/security/email/change_notice.html:4 msgid "click here to reset it" msgstr "" #: flask_security/templates/security/email/change_notice.txt:3 msgid "If you did not change your password, click the link below to reset it." msgstr "" #: flask_security/templates/security/email/change_username_notice.html:1 #: flask_security/templates/security/email/change_username_notice.txt:1 msgid "Your username has been changed." msgstr "Your username has been changed. OKAY!" #: flask_security/templates/security/email/confirmation_instructions.html:9 #: flask_security/templates/security/email/welcome.html:10 #, python-format msgid "" "Use this link to confirm your email" " address." msgstr "" "Use this link to confirm your email" " address. OKAY!" #: flask_security/templates/security/email/confirmation_instructions.txt:9 #: flask_security/templates/security/email/welcome.txt:11 #, python-format msgid "Use %(confirmation_link)s to confirm your email address." msgstr "" #: flask_security/templates/security/email/login_instructions.html:1 #: flask_security/templates/security/email/login_instructions.txt:1 #: flask_security/templates/security/email/welcome.html:8 #: flask_security/templates/security/email/welcome.txt:8 #, python-format msgid "Welcome %(email)s!" msgstr "Welcome %(email)s OKAY!" #: flask_security/templates/security/email/login_instructions.html:2 #: flask_security/templates/security/email/login_instructions.txt:3 msgid "You can log into your account through the link below:" msgstr "" #: flask_security/templates/security/email/login_instructions.html:4 msgid "Login now" msgstr "" #: flask_security/templates/security/email/reset_instructions.html:9 #, python-format msgid "Use this link to reset your password." msgstr "" #: flask_security/templates/security/email/reset_instructions.txt:9 #, python-format msgid "Use %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/reset_notice.html:1 #: flask_security/templates/security/email/reset_notice.txt:1 msgid "Your password has been reset." msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:1 #: flask_security/templates/security/email/two_factor_instructions.txt:1 #: flask_security/templates/security/email/us_instructions.html:10 #: flask_security/templates/security/email/us_instructions.txt:10 #, python-format msgid "Welcome %(username)s!" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:2 #: flask_security/templates/security/email/two_factor_instructions.txt:3 #, python-format msgid "You can log into your account using the following code: %(token)s" msgstr "" #: flask_security/templates/security/email/two_factor_instructions.html:3 #: flask_security/templates/security/email/two_factor_instructions.txt:5 #, python-format msgid "This code will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/two_factor_rescue.html:1 #: flask_security/templates/security/email/two_factor_rescue.txt:1 msgid "can not access mail account" msgstr "" #: flask_security/templates/security/email/us_instructions.html:11 #: flask_security/templates/security/email/us_instructions.txt:11 #, python-format msgid "You can sign in to your account using the following code: %(code)s" msgstr "" #: flask_security/templates/security/email/us_instructions.html:13 #, python-format msgid "Or use this link: Sign in" msgstr "" #: flask_security/templates/security/email/us_instructions.html:15 #: flask_security/templates/security/email/us_instructions.txt:15 #, python-format msgid "This code/link will expire in %(within)s." msgstr "" #: flask_security/templates/security/email/us_instructions.txt:13 #, python-format msgid "Or use this link: %(login_link)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:5 #: flask_security/templates/security/email/username_recovery.txt:5 msgid "Hello," msgstr "" #: flask_security/templates/security/email/username_recovery.html:6 #: flask_security/templates/security/email/username_recovery.txt:6 msgid "You recently requested to recover your username." msgstr "" #: flask_security/templates/security/email/username_recovery.html:7 #: flask_security/templates/security/email/username_recovery.txt:7 #, python-format msgid "Your username is: %(username)s" msgstr "" #: flask_security/templates/security/email/username_recovery.html:8 #: flask_security/templates/security/email/username_recovery.txt:8 msgid "If you did not initiate this request, you can safely ignore this email." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:16 #: flask_security/templates/security/email/welcome_existing.txt:16 #: flask_security/templates/security/email/welcome_existing_username.html:11 #: flask_security/templates/security/email/welcome_existing_username.txt:11 #, python-format msgid "Hello %(email)s!" msgstr "" #: flask_security/templates/security/email/welcome_existing.html:17 #: flask_security/templates/security/email/welcome_existing.txt:18 msgid "" "Someone (you?) tried to register this email - which is already in our " "system." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:20 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:24 #, python-format msgid "" "You can use this link to reset your " "password." msgstr "" #: flask_security/templates/security/email/welcome_existing.html:27 #, python-format msgid "" "You have not confirmed your email address yet - use this link to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:21 #, python-format msgid "" "This account also has the following username associated with it: " "%(username)s" msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:25 #, python-format msgid "You can use this link %(reset_link)s to reset your password." msgstr "" #: flask_security/templates/security/email/welcome_existing.txt:29 #, python-format msgid "" "You have not confirmed your email address yet - use this link: " "%(confirmation_link)s to do so now." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:13 #: flask_security/templates/security/email/welcome_existing_username.txt:13 #, python-format msgid "" "You attempted to register with a username \"%(username)s\" that is " "already associated with another account." msgstr "" #: flask_security/templates/security/email/welcome_existing_username.html:15 #: flask_security/templates/security/email/welcome_existing_username.txt:16 msgid "Please restart the registration process with a different username." msgstr "" pallets-eco-flask-security-72bf929/tests/translations/fr_FR/000077500000000000000000000000001525721743500241275ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/tests/translations/fr_FR/LC_MESSAGES/000077500000000000000000000000001525721743500257145ustar00rootroot00000000000000pallets-eco-flask-security-72bf929/tests/translations/fr_FR/LC_MESSAGES/flask_security.po000066400000000000000000000001661525721743500313060ustar00rootroot00000000000000msgid "" msgstr "" msgid "Password no-worky" msgstr "Passe - no-worky" msgid "Password" msgstr "Mot de passe OKAY!" pallets-eco-flask-security-72bf929/tests/view_scaffold.py000066400000000000000000000306231525721743500236010ustar00rootroot00000000000000# :copyright: (c) 2019-2026 by J. Christopher Wagner (jwag). # :license: MIT, see LICENSE for more details. """ This is a simple scaffold that can be run as an app and manually test various views using a browser. It can be used to test translations by adding ?lang=xx. You might need to delete the session cookie if you need to switch between languages (it is easy to do this with your browser development tools). Configurations can be set via environment variables. Runs on port 5001 An initial user: test@test.com/password is created. If you want to register a new user - you will receive a 'flash' that has the confirm URL (with token) you need to enter into your browser address bar. Since we don't actually send email - we have signal handlers flash the required data and a mail sender that flashes what mail would be sent! """ from __future__ import annotations import base64 import secrets from datetime import timedelta import os import typing as t import webbrowser from flask import Flask, flash, render_template_string, request, session, g from flask_security import ( MailUtil, Security, UserDatastore, UserMixin, WebauthnUtil, auth_required, current_user, SQLAlchemyUserDatastore, FSQLALiteUserDatastore, ) from flask_security.signals import ( us_security_token_sent, tf_security_token_sent, reset_password_instructions_sent, user_not_registered, user_registered, ) from flask_security.utils import ( hash_password, naive_utcnow, uia_email_mapper, uia_phone_mapper, ) def _find_bool(v): if str(v).lower() in ["true"]: return True elif str(v).lower() in ["false"]: return False return v class FlashMailUtil(MailUtil): def send_mail( self, template: str, subject: str, recipient: str | tuple[str, str], sender: str | tuple[str, str], body: str | None, html: str | None, **kwargs: t.Any, ) -> None: flash(f"Email body: {body}") if html: hb = html.encode() url = "data:text/html;base64," + base64.b64encode(hb).decode() webbrowser.open(url, new=1) SET_LANG = False def fsqla_datastore(app): from flask_sqlalchemy import SQLAlchemy from flask_security.models import fsqla_v3 as fsqla from sqlalchemy_utils import database_exists, create_database # Create database models and hook up. app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False app.config.setdefault("SQLALCHEMY_DATABASE_URI", "sqlite:///:memory:") db = SQLAlchemy(app) fsqla.FsModels.set_db_info(db) class Role(db.Model, fsqla.FsRoleMixin): pass class User(db.Model, fsqla.FsUserMixin): pass class WebAuthn(db.Model, fsqla.FsWebAuthnMixin): pass with app.app_context(): if not database_exists(db.engine.url): create_database(db.engine.url) db.create_all() return SQLAlchemyUserDatastore(db, User, Role, WebAuthn) def fsqla_lite_datastore(app: Flask) -> FSQLALiteUserDatastore: from sqlalchemy.orm import DeclarativeBase from flask_sqlalchemy_lite import SQLAlchemy from flask_security.models import sqla as sqla from sqlalchemy_utils import database_exists, create_database # Create database models and hook up. app.config.setdefault("SQLALCHEMY_DATABASE_URI", "sqlite:///:memory:") app.config |= { "SQLALCHEMY_ENGINES": { "default": { "url": app.config["SQLALCHEMY_DATABASE_URI"], "pool_pre_ping": True, }, }, } db = SQLAlchemy(app) class Model(DeclarativeBase): pass sqla.FsModels.set_db_info(base_model=Model) class Role(Model, sqla.FsRoleMixin): __tablename__ = "role" pass class User(Model, sqla.FsUserMixin): __tablename__ = "user" pass class WebAuthn(Model, sqla.FsWebAuthnMixin): __tablename__ = "web_authn" # N.B. this is name that Flask-SQLAlchemy gives. pass with app.app_context(): if not database_exists(db.engine.url): create_database(db.engine.url) Model.metadata.create_all(db.engine) return FSQLALiteUserDatastore(db, User, Role, WebAuthn) def create_app() -> Flask: # Use real templates - not test templates... app = Flask("view_scaffold", template_folder="../") app.config["SERVER_NAME"] = "localhost" app.config["DEBUG"] = True # SECRET_KEY generated using: secrets.token_urlsafe() app.config["SECRET_KEY"] = "pf9Wkove4IKEAXvy-cQkeDPhv9Cb3Ag-wyJILbq_dFw" # PASSWORD_SALT secrets.SystemRandom().getrandbits(128) app.config["SECURITY_PASSWORD_SALT"] = "156043940537155509276282232127182067465" app.config["LOGIN_DISABLED"] = False app.config["REMEMBER_COOKIE_SAMESITE"] = "strict" # 'strict' causes redirect after OAuth to fail since session cookie not sent # this just happens on first 'register' with e.g. github # app.config["SESSION_COOKIE_SAMESITE"] = "strict" app.config["SECURITY_USER_IDENTITY_ATTRIBUTES"] = [ {"email": {"mapper": uia_email_mapper, "case_insensitive": True}}, {"us_phone_number": {"mapper": uia_phone_mapper}}, ] # app.config["SECURITY_US_ENABLED_METHODS"] = ["password"] # app.config["SECURITY_US_ENABLED_METHODS"] = ["authenticator", "password"] # app.config["SECURITY_US_SIGNIN_REPLACES_LOGIN"] = True # app.config["SECURITY_WAN_ALLOW_USER_HINTS"] = False # Setup script nonces and test with nonce-based content-security policy app.config["SECURITY_SCRIPT_NONCE_KEY"] = "csp_nonce" @app.before_request def set_nonce(): g.csp_nonce = secrets.token_urlsafe(16) @app.after_request def inject_csp_header(response): response.headers["Content-Security-Policy"] = ( f"script-src 'nonce-{g.csp_nonce}'" ) return response app.config["SECURITY_TOTP_SECRETS"] = { "1": "TjQ9Qa31VOrfEzuPy4VHQWPCTmRzCnFzMKLxXYiZu9B" } app.config["SECURITY_TOTP_ISSUER"] = "me" app.config["SECURITY_FRESHNESS"] = timedelta(minutes=10) app.config["SECURITY_FRESHNESS_GRACE_PERIOD"] = timedelta(minutes=20) app.config["SECURITY_USERNAME_ENABLE"] = True app.config["SECURITY_USERNAME_REQUIRED"] = True app.config["SECURITY_PASSWORD_REQUIRED"] = False # allow registration w/o password app.config["SECURITY_RETURN_GENERIC_RESPONSES"] = False # enable OAuth - note that this assumes that app is passes XXX_CLIENT_ID and # XXX_CLIENT_SECRET as environment variables. app.config["SECURITY_OAUTH_ENABLE"] = True # app.config["SECURITY_URL_PREFIX"] = "/fs" class TestWebauthnUtil(WebauthnUtil): def generate_challenge(self, nbytes: int | None = None) -> str: # Use a constant Challenge so we can use this app to generate gold # responses for use in unit testing. See test_webauthn. # NEVER NEVER NEVER do this in production return "smCCiy_k2CqQydSQ_kPEjV5a2d0ApfatcpQ1aXDmQPo" def origin(self) -> str: # Return the RP origin - normally this is just the URL of the application. # To test with ngrok - we need the https address that the browser originally # sent - it is sent as the ORIGIN header - not sure if this should be # default or just for testing. return request.origin if request.origin else request.host_url.rstrip("/") # Turn on all features (except passwordless since that removes normal login) for opt in [ "changeable", "change_email", "change_username", "recoverable", "registerable", "trackable", "NOTpasswordless", "confirmable", "two_factor", "username_recovery", "unified_signin", "webauthn", "multi_factor_recovery_codes", ]: app.config["SECURITY_" + opt.upper()] = True if os.environ.get("SETTINGS"): # Load settings from a file pointed to by SETTINGS app.config.from_envvar("SETTINGS") # Allow any SECURITY_, SQLALCHEMY, Authlib config to be set in environment. for ev in os.environ: if ( ev.startswith("SECURITY_") or ev.startswith("SQLALCHEMY_") or "_CLIENT_" in ev ): app.config[ev] = _find_bool(os.environ.get(ev)) app.config["SECURITY_LOGOUT_CSRF"] = True # app.config["WTF_CSRF_CHECK_DEFAULT"] = False # CSRFProtect(app) # Setup Flask-Security # user_datastore = fsqla_datastore(app) user_datastore = fsqla_lite_datastore(app) security = Security( app, user_datastore, webauthn_util_cls=TestWebauthnUtil, mail_util_cls=FlashMailUtil, ) # Setup Babel def get_locale(): # For a given session - set lang based on first request. # Honor explicit url request first if not session: # if running CLI return global SET_LANG if not SET_LANG: session.pop("lang", None) SET_LANG = True if "lang" not in session: locale = request.args.get("lang", None) if not locale: locale = request.accept_languages.best if not locale: locale = "en" if locale: session["lang"] = locale return session.get("lang", None).replace("-", "_") try: import flask_babel flask_babel.Babel(app, locale_selector=get_locale) except ImportError: pass @user_registered.connect_via(app) def on_user_registered( myapp: Flask, user: UserMixin, confirm_token: str, **extra: dict[str, t.Any] ) -> None: flash(f"To confirm {user.email} - go to /confirm/{confirm_token}") @user_not_registered.connect_via(app) def on_user_not_registered(myapp, **extra): if extra.get("existing_email"): flash(f"Tried to register existing email: {extra['user'].email}") elif extra.get("existing_username"): flash( f"Tried to register email: {extra['form_data'].email.data}" f" with username: {extra['form_data'].username.data}" ) else: flash("Not registered response - but ??") @reset_password_instructions_sent.connect_via(app) def on_reset(myapp, user, token, **extra): flash(f"Go to /reset/{token}") @tf_security_token_sent.connect_via(app) def on_token_sent(myapp, user, token, method, **extra): flash( "User {} was sent two factor token {} via {}".format( user.calc_username(), token, method ) ) @us_security_token_sent.connect_via(app) def on_us_token_sent(myapp, user, token, method, **extra): flash( "User {} was sent sign in code {} via {}".format( user.calc_username(), token, method ) ) # Views @app.route("/") @auth_required() def home(): return render_template_string( """ {% extends "security/base.html" %} {% block content %} {% include 'security/_messages.html' %} {{ _fsdomain('Welcome') }} {{email}} ! {% include "security/_menu.html" %} {% endblock content %} """, email=current_user.email, security=security, ) @app.route("/basicauth") @auth_required("basic") def basic(): return render_template_string("Basic auth success") @app.route("/protected") @auth_required() def protected(): return render_template_string("Protected endpoint") return app def add_user( ds: UserDatastore, email: str, password: str, role_names: list[str] ) -> None: pw = hash_password(password) roles = [ds.find_or_create_role(rn) for rn in role_names] ds.commit() user = ds.create_user( email=email, password=pw, active=True, confirmed_at=naive_utcnow() ) ds.commit() for role in roles: ds.add_role_to_user(user, role) ds.commit() if __name__ == "__main__": myapp = create_app() security: Security = myapp.extensions["security"] with myapp.app_context(): test_acct = "test@test.com" if not security.datastore.find_user(email=test_acct): add_user(security.datastore, test_acct, "password", ["admin"]) print("Created User: {} with password {}".format(test_acct, "password")) myapp.run(port=5001) pallets-eco-flask-security-72bf929/tox.ini000066400000000000000000000115771525721743500205740ustar00rootroot00000000000000[tox] basepython = python3.13 envlist = py{311,312,313,314,py311}-{low,release,appinstall} mypy async nowebauthn nobabel noauthlib style docs coverage makedist skip_missing_interpreters = false [testenv] allowlist_externals = tox [testenv:pypy311-release] dependency_groups = tests commands = tox -e compile_catalog pytest -W ignore --basetemp={envtmpdir} {posargs:tests} [testenv:py{311,312,313,314}-release] dependency_groups = tests commands = tox -e compile_catalog pytest --basetemp={envtmpdir} {posargs:tests} [testenv:py{311,312,313,314,py311}-low] deps = pytest dependency_groups = low commands = tox -e compile_catalog pytest -W ignore --basetemp={envtmpdir} {posargs:tests} # manual test to check how we're keeping up with Pallets latest [testenv:main] basepython = python3.14 dependency_groups = tests deps = git+https://github.com/pallets/werkzeug@main#egg=werkzeug git+https://github.com/pallets/flask@main#egg=flask git+https://github.com/pallets/flask-sqlalchemy@main#egg=flask-sqlalchemy git+https://github.com/pallets/jinja@main#egg=jinja2 git+https://github.com/pallets-eco/flask-principal@main#egg=flask-principal git+https://github.com/wtforms/wtforms@main#egg=wtforms git+https://github.com/maxcountryman/flask-login@main#egg=flask-login commands = tox -e compile_catalog pytest --basetemp={envtmpdir} {posargs:tests} [testenv:async] dependency_groups = tests commands = pip install flask[async] tox -e compile_catalog pytest --basetemp={envtmpdir} {posargs:tests} [testenv:nowebauthn] dependency_groups = tests commands = pip uninstall -y webauthn tox -e compile_catalog pytest --basetemp={envtmpdir} {posargs:tests} [testenv:nobabel] dependency_groups = tests commands = pip uninstall -y babel flask_babel pytest --basetemp={envtmpdir} {posargs:tests} [testenv:noauthlib] dependency_groups = tests commands = pip uninstall -y authlib tox -e compile_catalog pytest --basetemp={envtmpdir} {posargs:tests} [testenv:noflasksqlalchemy] dependency_groups = tests commands = pip uninstall -y flask_sqlalchemy flask_sqlalchemy_lite tox -e compile_catalog pytest --basetemp={envtmpdir} {posargs:tests} [testenv:style] deps = pre-commit skip_install = true commands = pre-commit run --all-files --show-diff-on-failure [testenv:docs] dependency_groups = docs commands = sphinx-build -E -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html [testenv:update-actions] description = Update GitHub Action Pins labels = update deps = gha-update; python_full_version >= '3.12' skip_install = true commands = gha-update [testenv:update-pre_commit] description = Update pre-commit Pins labels = update deps = pre-commit skip_install = true commands = pre-commit autoupdate --freeze -j4 [testenv:coverage] dependency_groups = tests commands = tox -e compile_catalog coverage run --source=flask_security -m pytest coverage xml [testenv:realpostgres] dependency_groups = tests deps = psycopg2-binary commands = # Requires that --realdburl be set otherwise doesn't actually use DB tox -e compile_catalog pytest --basetemp={envtmpdir} {posargs} [testenv:makedist] dependency_groups = tests deps = flit twine check-wheel-contents commands = tox -e compile_catalog flit build --no-use-vcs check-wheel-contents dist [testenv:makedist-too] dependency_groups = tests deps = flit twine check-wheel-contents commands = tox -e compile_catalog flit -f pyproject-too.toml build --no-use-vcs check-wheel-contents dist [testenv:mypy] dependency_groups = tests typing commands = mypy --install-types --non-interactive flask_security tests # basedpyright --verifytypes flask_security --ignoreexternal [testenv:py{311,312,313,314}-appinstall] commands = python -c "from flask_security import Security; s = Security()" [testenv:compile_catalog] deps = babel skip_install = true commands = pybabel compile --domain flask_security -d flask_security/translations pybabel compile --domain flask_security -d tests/translations [testenv:extract_messages] deps = babel jinja2 skip_install = true commands = pybabel extract --version 5.9.1 --keyword=_fsdomain --project=Flask-Security \ -o flask_security/translations/flask_security.pot \ --msgid-bugs-address=jwag956@github.com --mapping-file=babel.ini \ --add-comments=NOTE flask_security [testenv:update_catalog] deps = babel skip_install = true commands = pybabel update --domain flask_security -i flask_security/translations/flask_security.pot \ -d flask_security/translations --no-fuzzy-matching pybabel update --domain flask_security -l cic_US -i flask_security/translations/flask_security.pot \ -d tests/translations --no-fuzzy-matching [flake8] max-line-length = 88 per-file-ignores = tests/view_scaffold.py: E402