Embeds the previous version of flask_security
Sophie Brun
3 years ago
38 | 38 | python3-syslog-rfc5424-formatter, |
39 | 39 | python3-tqdm, |
40 | 40 | python3-twisted, |
41 | python3-webargs (>= 7.0.0) | |
41 | python3-webargs (>= 7.0.0), | |
42 | # for flask-security embedded: | |
43 | python3-flask-babelex, | |
44 | python3-flask-login, | |
45 | python3-flask-mail, | |
46 | python3-passlib, | |
47 | python3-flaskext.wtf, | |
48 | python3-itsdangerous, | |
49 | python3-speaklater | |
42 | 50 | Standards-Version: 4.5.0 |
43 | 51 | Homepage: https://faradaysec.com |
44 | 52 | Vcs-Git: https://gitlab.com/kalilinux/packages/python-faraday.git |
59 | 67 | sudo, |
60 | 68 | xdg-utils, |
61 | 69 | zsh | zsh-beta, |
70 | # for flask-security embedded: | |
71 | python3-flask-babelex, | |
72 | python3-flask-login, | |
73 | python3-flask-mail, | |
74 | python3-passlib, | |
75 | python3-flaskext.wtf, | |
76 | python3-itsdangerous, | |
77 | python3-speaklater, | |
62 | 78 | ${misc:Depends}, |
63 | 79 | ${shlibs:Depends} |
64 | 80 | Recommends: fonts-font-awesome, |
1 | 1 | debian/migrate-database usr/lib/python3/dist-packages/faraday/debian-scripts |
2 | 2 | usr/lib/python3* |
3 | 3 | usr/bin |
4 | debian/python-modules/* usr/lib/python3/dist-packages/faraday |
0 | From: Sophie Brun <[email protected]> | |
1 | Date: Thu, 11 Feb 2021 11:07:56 +0100 | |
2 | Subject: Use local flask-security | |
3 | ||
4 | --- | |
5 | faraday/manage.py | 2 +- | |
6 | faraday/server/api/modules/token.py | 2 +- | |
7 | faraday/server/app.py | 6 +++--- | |
8 | faraday/server/commands/change_password.py | 4 ++-- | |
9 | faraday/server/commands/initdb.py | 2 +- | |
10 | faraday/server/models.py | 2 +- | |
11 | tests/test_command_change_password.py | 4 ++-- | |
12 | 7 files changed, 11 insertions(+), 11 deletions(-) | |
13 | ||
14 | diff --git a/faraday/manage.py b/faraday/manage.py | |
15 | index 973dc59..b84a1ab 100755 | |
16 | --- a/faraday/manage.py | |
17 | +++ b/faraday/manage.py | |
18 | @@ -53,7 +53,7 @@ from faraday.server.commands import import_vulnerability_template | |
19 | from faraday.server.models import db, User | |
20 | from faraday.server.web import app | |
21 | from faraday_plugins.plugins.manager import PluginsManager | |
22 | -from flask_security.utils import hash_password | |
23 | +from faraday.flask_security.utils import hash_password | |
24 | ||
25 | ||
26 | CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) | |
27 | diff --git a/faraday/server/api/modules/token.py b/faraday/server/api/modules/token.py | |
28 | index fe8e224..4ffd763 100644 | |
29 | --- a/faraday/server/api/modules/token.py | |
30 | +++ b/faraday/server/api/modules/token.py | |
31 | @@ -1,6 +1,6 @@ | |
32 | from itsdangerous import TimedJSONWebSignatureSerializer | |
33 | from flask import Blueprint, g | |
34 | -from flask_security.utils import hash_data | |
35 | +from faraday.flask_security.utils import hash_data | |
36 | from flask import current_app as app | |
37 | ||
38 | ||
39 | diff --git a/faraday/server/app.py b/faraday/server/app.py | |
40 | index 6ff58f3..697416a 100644 | |
41 | --- a/faraday/server/app.py | |
42 | +++ b/faraday/server/app.py | |
43 | @@ -17,12 +17,12 @@ import flask | |
44 | from flask import Flask, session, g, request | |
45 | from flask.json import JSONEncoder | |
46 | from flask_sqlalchemy import get_debug_queries | |
47 | -from flask_security import ( | |
48 | +from faraday.flask_security import ( | |
49 | Security, | |
50 | SQLAlchemyUserDatastore, | |
51 | ) | |
52 | -from flask_security.forms import LoginForm | |
53 | -from flask_security.utils import ( | |
54 | +from faraday.flask_security.forms import LoginForm | |
55 | +from faraday.flask_security.utils import ( | |
56 | _datastore, | |
57 | get_message, | |
58 | verify_and_update_password, | |
59 | diff --git a/faraday/server/commands/change_password.py b/faraday/server/commands/change_password.py | |
60 | index 42a7b1d..cfd6559 100644 | |
61 | --- a/faraday/server/commands/change_password.py | |
62 | +++ b/faraday/server/commands/change_password.py | |
63 | @@ -1,6 +1,6 @@ | |
64 | from faraday.server.web import app | |
65 | from faraday.server.models import User, db | |
66 | -from flask_security.utils import hash_password | |
67 | +from faraday.flask_security.utils import hash_password | |
68 | ||
69 | ||
70 | def changes_password(username, password): | |
71 | @@ -13,4 +13,4 @@ def changes_password(username, password): | |
72 | print("Password changed succesfully") | |
73 | else: | |
74 | print("User not found in Faraday's Database") | |
75 | -# I'm Py3 | |
76 | \ No newline at end of file | |
77 | +# I'm Py3 | |
78 | diff --git a/faraday/server/commands/initdb.py b/faraday/server/commands/initdb.py | |
79 | index d09b84b..175c369 100644 | |
80 | --- a/faraday/server/commands/initdb.py | |
81 | +++ b/faraday/server/commands/initdb.py | |
82 | @@ -28,7 +28,7 @@ from faraday.server.utils.database import is_unique_constraint_violation | |
83 | from configparser import ConfigParser, NoSectionError | |
84 | ||
85 | from flask import current_app | |
86 | -from flask_security.utils import hash_password | |
87 | +from faraday.flask_security.utils import hash_password | |
88 | ||
89 | from colorama import init | |
90 | from colorama import Fore | |
91 | diff --git a/faraday/server/models.py b/faraday/server/models.py | |
92 | index 40dbe44..e3ad6ab 100644 | |
93 | --- a/faraday/server/models.py | |
94 | +++ b/faraday/server/models.py | |
95 | @@ -46,7 +46,7 @@ from flask_sqlalchemy import ( | |
96 | from depot.fields.sqlalchemy import UploadedFileField | |
97 | ||
98 | from faraday.server.fields import JSONType | |
99 | -from flask_security import ( | |
100 | +from faraday.flask_security import ( | |
101 | UserMixin, | |
102 | ) | |
103 | ||
104 | diff --git a/tests/test_command_change_password.py b/tests/test_command_change_password.py | |
105 | index 99efcbd..0bec8c7 100644 | |
106 | --- a/tests/test_command_change_password.py | |
107 | +++ b/tests/test_command_change_password.py | |
108 | @@ -1,4 +1,4 @@ | |
109 | -from flask_security.utils import hash_password, verify_password | |
110 | +from faraday.flask_security.utils import hash_password, verify_password | |
111 | ||
112 | from faraday.server.commands.change_password import changes_password | |
113 | from faraday.server.models import User | |
114 | @@ -15,4 +15,4 @@ def test_changes_password_command(session): | |
115 | user = User.query.filter_by(username='test_change_pass').first() | |
116 | ||
117 | assert not verify_password('old_pass', user.password) | |
118 | - assert verify_password('new_pass', user.password) | |
119 | \ No newline at end of file | |
120 | + assert verify_password('new_pass', user.password) |
2 | 2 | use-packaged-filteralchemy.patch |
3 | 3 | Remove-failing-tests.patch |
4 | 4 | fix-flask-security-requirement.patch |
5 | Use-local-flask-security.patch |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security | |
3 | ~~~~~~~~~~~~~~ | |
4 | ||
5 | Flask-Security is a Flask extension that aims to add quick and simple | |
6 | security via Flask-Login, Flask-Principal, Flask-WTF, and passlib. | |
7 | ||
8 | :copyright: (c) 2012-2019 by Matt Wright. | |
9 | :copyright: (c) 2019-2020 by J. Christopher Wagner. | |
10 | :license: MIT, see LICENSE for more details. | |
11 | """ | |
12 | ||
13 | # flake8: noqa: F401 | |
14 | ||
15 | from .core import Security, RoleMixin, UserMixin, AnonymousUser, current_user | |
16 | from .datastore import ( | |
17 | UserDatastore, | |
18 | SQLAlchemyUserDatastore, | |
19 | MongoEngineUserDatastore, | |
20 | PeeweeUserDatastore, | |
21 | PonyUserDatastore, | |
22 | SQLAlchemySessionUserDatastore, | |
23 | ) | |
24 | from .decorators import ( | |
25 | auth_token_required, | |
26 | anonymous_user_required, | |
27 | handle_csrf, | |
28 | http_auth_required, | |
29 | login_required, | |
30 | roles_accepted, | |
31 | roles_required, | |
32 | auth_required, | |
33 | permissions_accepted, | |
34 | permissions_required, | |
35 | unauth_csrf, | |
36 | ) | |
37 | from .forms import ( | |
38 | ChangePasswordForm, | |
39 | ForgotPasswordForm, | |
40 | LoginForm, | |
41 | RegisterForm, | |
42 | ResetPasswordForm, | |
43 | PasswordlessLoginForm, | |
44 | ConfirmRegisterForm, | |
45 | SendConfirmationForm, | |
46 | TwoFactorRescueForm, | |
47 | TwoFactorSetupForm, | |
48 | TwoFactorVerifyCodeForm, | |
49 | TwoFactorVerifyPasswordForm, | |
50 | VerifyForm, | |
51 | ) | |
52 | from .phone_util import PhoneUtil | |
53 | from .signals import ( | |
54 | confirm_instructions_sent, | |
55 | login_instructions_sent, | |
56 | password_changed, | |
57 | password_reset, | |
58 | reset_password_instructions_sent, | |
59 | tf_code_confirmed, | |
60 | tf_profile_changed, | |
61 | tf_security_token_sent, | |
62 | tf_disabled, | |
63 | user_authenticated, | |
64 | user_confirmed, | |
65 | user_registered, | |
66 | us_security_token_sent, | |
67 | us_profile_changed, | |
68 | ) | |
69 | from .totp import Totp | |
70 | from .twofactor import tf_send_security_token | |
71 | from .unified_signin import ( | |
72 | UnifiedSigninForm, | |
73 | UnifiedSigninSetupForm, | |
74 | UnifiedSigninSetupValidateForm, | |
75 | UnifiedVerifyForm, | |
76 | us_send_security_token, | |
77 | ) | |
78 | from .utils import ( | |
79 | FsJsonEncoder, | |
80 | SmsSenderBaseClass, | |
81 | SmsSenderFactory, | |
82 | check_and_get_token_status, | |
83 | get_hmac, | |
84 | get_token_status, | |
85 | get_url, | |
86 | hash_password, | |
87 | check_and_update_authn_fresh, | |
88 | login_user, | |
89 | logout_user, | |
90 | password_breached_validator, | |
91 | password_complexity_validator, | |
92 | password_length_validator, | |
93 | pwned, | |
94 | send_mail, | |
95 | transform_url, | |
96 | uia_phone_mapper, | |
97 | uia_email_mapper, | |
98 | url_for_security, | |
99 | verify_password, | |
100 | verify_and_update_password, | |
101 | ) | |
102 | ||
103 | __version__ = "3.4.2" |
0 | """ | |
1 | Temporary workaround while we still support p2.7 | |
2 | ||
3 | :copyright: (c) 2019 by J. Christopher Wagner (jwag). | |
4 | :license: MIT, see LICENSE for more details. | |
5 | """ | |
6 | ||
7 | from flask import current_app | |
8 | from werkzeug.local import LocalProxy | |
9 | ||
10 | _security = LocalProxy(lambda: current_app.extensions["security"]) | |
11 | ||
12 | _datastore = LocalProxy(lambda: _security.datastore) | |
13 | ||
14 | ||
15 | async def _commit(response=None): # pragma: no cover | |
16 | _datastore.commit() | |
17 | return response |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.babel | |
3 | ~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | I18N support for Flask-Security. | |
6 | """ | |
7 | ||
8 | from flask_babelex import Domain | |
9 | from wtforms.i18n import messages_path | |
10 | ||
11 | wtforms_domain = Domain(messages_path(), domain="wtforms") | |
12 | ||
13 | ||
14 | class Translations(object): | |
15 | """Fixes WTForms translation support and uses wtforms translations.""" | |
16 | ||
17 | def gettext(self, string): | |
18 | return wtforms_domain.gettext(string) | |
19 | ||
20 | def ngettext(self, singular, plural, n): | |
21 | return wtforms_domain.ngettext(singular, plural, n) |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.cache | |
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Flask-Security token cache module | |
6 | ||
7 | :copyright: (c) 2019. | |
8 | :license: MIT, see LICENSE for more details. | |
9 | """ | |
10 | ||
11 | from .utils import config_value | |
12 | ||
13 | ||
14 | class VerifyHashCache: | |
15 | """Cache handler to make it quick password check by bypassing | |
16 | already checked passwords against exact same couple of token/password. | |
17 | This cache handler is more efficient on small apps that | |
18 | run on few processes as cache is only shared between threads.""" | |
19 | ||
20 | def __init__(self): | |
21 | ttl = config_value("VERIFY_HASH_CACHE_TTL", default=(60 * 5)) | |
22 | max_size = config_value("VERIFY_HASH_CACHE_MAX_SIZE", default=500) | |
23 | ||
24 | try: | |
25 | from cachetools import TTLCache | |
26 | ||
27 | self._cache = TTLCache(max_size, ttl) | |
28 | except ImportError: | |
29 | # this should have been checked at app init. | |
30 | raise | |
31 | ||
32 | def has_verify_hash_cache(self, user): | |
33 | """Check given user id is in cache.""" | |
34 | return self._cache.get(user.id) | |
35 | ||
36 | def set_cache(self, user): | |
37 | """When a password is checked, then result is put in cache.""" | |
38 | self._cache[user.id] = True | |
39 | ||
40 | def clear(self): | |
41 | """Clear cache""" | |
42 | self._cache.clear() |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.changeable | |
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Flask-Security recoverable module | |
6 | ||
7 | :copyright: (c) 2012 by Matt Wright. | |
8 | :author: Eskil Heyn Olsen | |
9 | :license: MIT, see LICENSE for more details. | |
10 | """ | |
11 | ||
12 | from flask import current_app | |
13 | from werkzeug.local import LocalProxy | |
14 | ||
15 | from .signals import password_changed | |
16 | from .utils import config_value, hash_password | |
17 | ||
18 | # Convenient references | |
19 | _security = LocalProxy(lambda: current_app.extensions["security"]) | |
20 | ||
21 | _datastore = LocalProxy(lambda: _security.datastore) | |
22 | ||
23 | ||
24 | def send_password_changed_notice(user): | |
25 | """Sends the password changed notice email for the specified user. | |
26 | ||
27 | :param user: The user to send the notice to | |
28 | """ | |
29 | if config_value("SEND_PASSWORD_CHANGE_EMAIL"): | |
30 | subject = config_value("EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE") | |
31 | _security._send_mail(subject, user.email, "change_notice", user=user) | |
32 | ||
33 | ||
34 | def change_user_password(user, password): | |
35 | """Change the specified user's password | |
36 | ||
37 | :param user: The user to change_password | |
38 | :param password: The unhashed new password | |
39 | """ | |
40 | user.password = hash_password(password) | |
41 | if config_value("BACKWARDS_COMPAT_AUTH_TOKEN_INVALID"): | |
42 | _datastore.set_uniquifier(user) | |
43 | _datastore.put(user) | |
44 | send_password_changed_notice(user) | |
45 | password_changed.send(current_app._get_current_object(), user=user) |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.cli | |
3 | ~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Command Line Interface for managing accounts and roles. | |
6 | ||
7 | :copyright: (c) 2016 by CERN. | |
8 | :copyright: (c) 2019 by J. Christopher Wagner | |
9 | :license: MIT, see LICENSE for more details. | |
10 | """ | |
11 | ||
12 | from __future__ import absolute_import, print_function | |
13 | ||
14 | from functools import wraps | |
15 | ||
16 | import click | |
17 | from flask import current_app | |
18 | from werkzeug.datastructures import MultiDict | |
19 | from werkzeug.local import LocalProxy | |
20 | from .quart_compat import get_quart_status | |
21 | ||
22 | from .utils import hash_password | |
23 | ||
24 | if get_quart_status(): # pragma: no cover | |
25 | import quart.cli | |
26 | import functools | |
27 | ||
28 | # quart cli doesn't provide the with_appcontext function | |
29 | def with_appcontext(f): | |
30 | """Wraps a callback so that it's guaranteed to be executed with the | |
31 | script's application context. If callbacks are registered directly | |
32 | to the ``app.cli`` object then they are wrapped with this function | |
33 | by default unless it's disabled. | |
34 | """ | |
35 | ||
36 | @click.pass_context | |
37 | def decorator(__ctx, *args, **kwargs): | |
38 | with __ctx.ensure_object(quart.cli.ScriptInfo).load_app().app_context(): | |
39 | return __ctx.invoke(f, *args, **kwargs) | |
40 | ||
41 | return functools.update_wrapper(decorator, f) | |
42 | ||
43 | ||
44 | else: | |
45 | import flask.cli | |
46 | ||
47 | with_appcontext = flask.cli.with_appcontext | |
48 | ||
49 | ||
50 | _security = LocalProxy(lambda: current_app.extensions["security"]) | |
51 | _datastore = LocalProxy(lambda: current_app.extensions["security"].datastore) | |
52 | ||
53 | ||
54 | def commit(fn): | |
55 | """Decorator to commit changes in datastore.""" | |
56 | ||
57 | @wraps(fn) | |
58 | def wrapper(*args, **kwargs): | |
59 | fn(*args, **kwargs) | |
60 | _datastore.commit() | |
61 | ||
62 | return wrapper | |
63 | ||
64 | ||
65 | @click.group() | |
66 | def users(): | |
67 | """User commands.""" | |
68 | ||
69 | ||
70 | @click.group() | |
71 | def roles(): | |
72 | """Role commands.""" | |
73 | ||
74 | ||
75 | @users.command("create") | |
76 | @click.argument("identity") | |
77 | @click.password_option() | |
78 | @click.option("-a", "--active", default=False, is_flag=True) | |
79 | @with_appcontext | |
80 | @commit | |
81 | def users_create(identity, password, active): | |
82 | """Create a user.""" | |
83 | kwargs = {attr: identity for attr in _security.user_identity_attributes} | |
84 | kwargs.update(**{"password": password}) | |
85 | ||
86 | form = _security.confirm_register_form(MultiDict(kwargs), meta={"csrf": False}) | |
87 | ||
88 | if form.validate(): | |
89 | kwargs["password"] = hash_password(kwargs["password"]) | |
90 | kwargs["active"] = active | |
91 | _datastore.create_user(**kwargs) | |
92 | click.secho("User created successfully.", fg="green") | |
93 | kwargs["password"] = "****" | |
94 | click.echo(kwargs) | |
95 | else: | |
96 | raise click.UsageError("Error creating user. %s" % form.errors) | |
97 | ||
98 | ||
99 | @roles.command("create") | |
100 | @click.argument("name") | |
101 | @click.option("-d", "--description", default=None) | |
102 | @click.option("-p", "--permissions") | |
103 | @with_appcontext | |
104 | @commit | |
105 | def roles_create(**kwargs): | |
106 | """Create a role.""" | |
107 | ||
108 | # For some reaosn Click puts arguments in kwargs - even if they weren't specified. | |
109 | if "permissions" in kwargs and not kwargs["permissions"]: | |
110 | del kwargs["permissions"] | |
111 | if "permissions" in kwargs and not hasattr(_datastore.role_model, "permissions"): | |
112 | raise click.UsageError("Role model does not support permissions") | |
113 | _datastore.create_role(**kwargs) | |
114 | click.secho('Role "%(name)s" created successfully.' % kwargs, fg="green") | |
115 | ||
116 | ||
117 | @roles.command("add") | |
118 | @click.argument("user") | |
119 | @click.argument("role") | |
120 | @with_appcontext | |
121 | @commit | |
122 | def roles_add(user, role): | |
123 | """Add user to role.""" | |
124 | user, role = _datastore._prepare_role_modify_args(user, role) | |
125 | if user is None: | |
126 | raise click.UsageError("Cannot find user.") | |
127 | if role is None: | |
128 | raise click.UsageError("Cannot find role.") | |
129 | if _datastore.add_role_to_user(user, role): | |
130 | click.secho( | |
131 | 'Role "{0}" added to user "{1}" ' "successfully.".format(role, user), | |
132 | fg="green", | |
133 | ) | |
134 | else: | |
135 | raise click.UsageError("Cannot add role to user.") | |
136 | ||
137 | ||
138 | @roles.command("remove") | |
139 | @click.argument("user") | |
140 | @click.argument("role") | |
141 | @with_appcontext | |
142 | @commit | |
143 | def roles_remove(user, role): | |
144 | """Remove user from role.""" | |
145 | user, role = _datastore._prepare_role_modify_args(user, role) | |
146 | if user is None: | |
147 | raise click.UsageError("Cannot find user.") | |
148 | if role is None: | |
149 | raise click.UsageError("Cannot find role.") | |
150 | if _datastore.remove_role_from_user(user, role): | |
151 | click.secho( | |
152 | 'Role "{0}" removed from user "{1}" ' "successfully.".format(role, user), | |
153 | fg="green", | |
154 | ) | |
155 | else: | |
156 | raise click.UsageError("Cannot remove role from user.") | |
157 | ||
158 | ||
159 | @users.command("activate") | |
160 | @click.argument("user") | |
161 | @with_appcontext | |
162 | @commit | |
163 | def users_activate(user): | |
164 | """Activate a user.""" | |
165 | user_obj = _datastore.get_user(user) | |
166 | if user_obj is None: | |
167 | raise click.UsageError("ERROR: User not found.") | |
168 | if _datastore.activate_user(user_obj): | |
169 | click.secho('User "{0}" has been activated.'.format(user), fg="green") | |
170 | else: | |
171 | click.secho('User "{0}" was already activated.'.format(user), fg="yellow") | |
172 | ||
173 | ||
174 | @users.command("deactivate") | |
175 | @click.argument("user") | |
176 | @with_appcontext | |
177 | @commit | |
178 | def users_deactivate(user): | |
179 | """Deactivate a user.""" | |
180 | user_obj = _datastore.get_user(user) | |
181 | if user_obj is None: | |
182 | raise click.UsageError("ERROR: User not found.") | |
183 | if _datastore.deactivate_user(user_obj): | |
184 | click.secho('User "{0}" has been deactivated.'.format(user), fg="green") | |
185 | else: | |
186 | click.secho('User "{0}" was already deactivated.'.format(user), fg="yellow") |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.confirmable | |
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Flask-Security confirmable module | |
6 | ||
7 | :copyright: (c) 2012 by Matt Wright. | |
8 | :copyright: (c) 2017 by CERN. | |
9 | :license: MIT, see LICENSE for more details. | |
10 | """ | |
11 | ||
12 | from flask import current_app as app | |
13 | from werkzeug.local import LocalProxy | |
14 | ||
15 | from .signals import confirm_instructions_sent, user_confirmed | |
16 | from .utils import ( | |
17 | config_value, | |
18 | get_token_status, | |
19 | hash_data, | |
20 | url_for_security, | |
21 | verify_hash, | |
22 | ) | |
23 | ||
24 | # Convenient references | |
25 | _security = LocalProxy(lambda: app.extensions["security"]) | |
26 | ||
27 | _datastore = LocalProxy(lambda: _security.datastore) | |
28 | ||
29 | ||
30 | def generate_confirmation_link(user): | |
31 | token = generate_confirmation_token(user) | |
32 | return url_for_security("confirm_email", token=token, _external=True), token | |
33 | ||
34 | ||
35 | def send_confirmation_instructions(user): | |
36 | """Sends the confirmation instructions email for the specified user. | |
37 | ||
38 | :param user: The user to send the instructions to | |
39 | """ | |
40 | ||
41 | confirmation_link, token = generate_confirmation_link(user) | |
42 | ||
43 | _security._send_mail( | |
44 | config_value("EMAIL_SUBJECT_CONFIRM"), | |
45 | user.email, | |
46 | "confirmation_instructions", | |
47 | user=user, | |
48 | confirmation_link=confirmation_link, | |
49 | ) | |
50 | ||
51 | confirm_instructions_sent.send(app._get_current_object(), user=user, token=token) | |
52 | ||
53 | ||
54 | def generate_confirmation_token(user): | |
55 | """Generates a unique confirmation token for the specified user. | |
56 | ||
57 | :param user: The user to work with | |
58 | """ | |
59 | data = [str(user.id), hash_data(user.email)] | |
60 | return _security.confirm_serializer.dumps(data) | |
61 | ||
62 | ||
63 | def requires_confirmation(user): | |
64 | """Returns `True` if the user requires confirmation.""" | |
65 | return ( | |
66 | _security.confirmable | |
67 | and not _security.login_without_confirmation | |
68 | and user.confirmed_at is None | |
69 | ) | |
70 | ||
71 | ||
72 | def confirm_email_token_status(token): | |
73 | """Returns the expired status, invalid status, and user of a confirmation | |
74 | token. For example:: | |
75 | ||
76 | expired, invalid, user = confirm_email_token_status('...') | |
77 | ||
78 | :param token: The confirmation token | |
79 | """ | |
80 | expired, invalid, user, token_data = get_token_status( | |
81 | token, "confirm", "CONFIRM_EMAIL", return_data=True | |
82 | ) | |
83 | if not invalid and user: | |
84 | user_id, token_email_hash = token_data | |
85 | invalid = not verify_hash(token_email_hash, user.email) | |
86 | return expired, invalid, user | |
87 | ||
88 | ||
89 | def confirm_user(user): | |
90 | """Confirms the specified user | |
91 | ||
92 | :param user: The user to confirm | |
93 | """ | |
94 | if user.confirmed_at is not None: | |
95 | return False | |
96 | user.confirmed_at = _security.datetime_factory() | |
97 | _datastore.put(user) | |
98 | user_confirmed.send(app._get_current_object(), user=user) | |
99 | return True |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.core | |
3 | ~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Flask-Security core module | |
6 | ||
7 | :copyright: (c) 2012 by Matt Wright. | |
8 | :copyright: (c) 2017 by CERN. | |
9 | :copyright: (c) 2017 by ETH Zurich, Swiss Data Science Center. | |
10 | :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag). | |
11 | :license: MIT, see LICENSE for more details. | |
12 | """ | |
13 | ||
14 | from datetime import datetime, timedelta | |
15 | import warnings | |
16 | import sys | |
17 | ||
18 | import pkg_resources | |
19 | from flask import _request_ctx_stack, current_app, render_template | |
20 | from flask_babelex import Domain | |
21 | from flask_login import AnonymousUserMixin, LoginManager | |
22 | from flask_login import UserMixin as BaseUserMixin | |
23 | from flask_login import current_user | |
24 | from flask_principal import Identity, Principal, RoleNeed, UserNeed, identity_loaded | |
25 | from itsdangerous import URLSafeTimedSerializer | |
26 | from passlib.context import CryptContext | |
27 | from werkzeug.datastructures import ImmutableList | |
28 | from werkzeug.local import LocalProxy, Local | |
29 | ||
30 | from .decorators import ( | |
31 | default_reauthn_handler, | |
32 | default_unauthn_handler, | |
33 | default_unauthz_handler, | |
34 | ) | |
35 | from .forms import ( | |
36 | ChangePasswordForm, | |
37 | ConfirmRegisterForm, | |
38 | ForgotPasswordForm, | |
39 | LoginForm, | |
40 | PasswordlessLoginForm, | |
41 | RegisterForm, | |
42 | ResetPasswordForm, | |
43 | SendConfirmationForm, | |
44 | TwoFactorVerifyCodeForm, | |
45 | TwoFactorSetupForm, | |
46 | TwoFactorVerifyPasswordForm, | |
47 | TwoFactorRescueForm, | |
48 | VerifyForm, | |
49 | ) | |
50 | from .phone_util import PhoneUtil | |
51 | from .twofactor import tf_send_security_token | |
52 | from .unified_signin import ( | |
53 | UnifiedSigninForm, | |
54 | UnifiedSigninSetupForm, | |
55 | UnifiedSigninSetupValidateForm, | |
56 | UnifiedVerifyForm, | |
57 | us_send_security_token, | |
58 | ) | |
59 | from .totp import Totp | |
60 | from .utils import _ | |
61 | from .utils import config_value as cv | |
62 | from .utils import ( | |
63 | FsJsonEncoder, | |
64 | FsPermNeed, | |
65 | csrf_cookie_handler, | |
66 | default_want_json, | |
67 | default_password_validator, | |
68 | get_config, | |
69 | get_identity_attributes, | |
70 | get_message, | |
71 | hash_data, | |
72 | localize_callback, | |
73 | send_mail, | |
74 | string_types, | |
75 | uia_email_mapper, | |
76 | uia_phone_mapper, | |
77 | url_for_security, | |
78 | verify_and_update_password, | |
79 | verify_hash, | |
80 | ) | |
81 | from .views import create_blueprint, default_render_json | |
82 | from .cache import VerifyHashCache | |
83 | ||
84 | # Convenient references | |
85 | _security = LocalProxy(lambda: current_app.extensions["security"]) | |
86 | _datastore = LocalProxy(lambda: _security.datastore) | |
87 | local_cache = Local() | |
88 | ||
89 | # List of authentication mechanisms supported. | |
90 | AUTHN_MECHANISMS = ("basic", "session", "token") | |
91 | ||
92 | ||
93 | #: Default Flask-Security configuration | |
94 | _default_config = { | |
95 | "BLUEPRINT_NAME": "security", | |
96 | "CLI_ROLES_NAME": "roles", | |
97 | "CLI_USERS_NAME": "users", | |
98 | "URL_PREFIX": None, | |
99 | "SUBDOMAIN": None, | |
100 | "FLASH_MESSAGES": True, | |
101 | "I18N_DOMAIN": "flask_security", | |
102 | "I18N_DIRNAME": pkg_resources.resource_filename("flask_security", "translations"), | |
103 | "PASSWORD_HASH": "bcrypt", | |
104 | "PASSWORD_SALT": None, | |
105 | "PASSWORD_SINGLE_HASH": { | |
106 | "django_argon2", | |
107 | "django_bcrypt_sha256", | |
108 | "django_pbkdf2_sha256", | |
109 | "django_pbkdf2_sha1", | |
110 | "django_bcrypt", | |
111 | "django_salted_md5", | |
112 | "django_salted_sha1", | |
113 | "django_des_crypt", | |
114 | "plaintext", | |
115 | }, | |
116 | "PASSWORD_SCHEMES": [ | |
117 | "bcrypt", | |
118 | "argon2", | |
119 | "des_crypt", | |
120 | "pbkdf2_sha256", | |
121 | "pbkdf2_sha512", | |
122 | "sha256_crypt", | |
123 | "sha512_crypt", | |
124 | # And always last one... | |
125 | "plaintext", | |
126 | ], | |
127 | "PASSWORD_HASH_OPTIONS": {}, # Deprecated at passlib 1.7 | |
128 | "PASSWORD_HASH_PASSLIB_OPTIONS": { | |
129 | "argon2__rounds": 10 # 1.7.1 default is 2. | |
130 | }, # >= 1.7.1 method to pass options. | |
131 | "PASSWORD_LENGTH_MIN": 8, | |
132 | "PASSWORD_COMPLEXITY_CHECKER": None, | |
133 | "PASSWORD_CHECK_BREACHED": False, | |
134 | "PASSWORD_BREACHED_COUNT": 1, | |
135 | "DEPRECATED_PASSWORD_SCHEMES": ["auto"], | |
136 | "LOGIN_URL": "/login", | |
137 | "LOGOUT_URL": "/logout", | |
138 | "REGISTER_URL": "/register", | |
139 | "RESET_URL": "/reset", | |
140 | "CHANGE_URL": "/change", | |
141 | "CONFIRM_URL": "/confirm", | |
142 | "VERIFY_URL": "/verify", | |
143 | "TWO_FACTOR_SETUP_URL": "/tf-setup", | |
144 | "TWO_FACTOR_TOKEN_VALIDATION_URL": "/tf-validate", | |
145 | "TWO_FACTOR_QRCODE_URL": "/tf-qrcode", | |
146 | "TWO_FACTOR_RESCUE_URL": "/tf-rescue", | |
147 | "TWO_FACTOR_CONFIRM_URL": "/tf-confirm", | |
148 | "LOGOUT_METHODS": ["GET", "POST"], | |
149 | "POST_LOGIN_VIEW": "/", | |
150 | "POST_LOGOUT_VIEW": "/", | |
151 | "CONFIRM_ERROR_VIEW": None, | |
152 | "POST_REGISTER_VIEW": None, | |
153 | "POST_CONFIRM_VIEW": None, | |
154 | "POST_RESET_VIEW": None, | |
155 | "POST_CHANGE_VIEW": None, | |
156 | "POST_VERIFY_VIEW": None, | |
157 | "UNAUTHORIZED_VIEW": None, | |
158 | "RESET_ERROR_VIEW": None, | |
159 | "RESET_VIEW": None, | |
160 | "LOGIN_ERROR_VIEW": None, | |
161 | "REDIRECT_HOST": None, | |
162 | "REDIRECT_BEHAVIOR": None, | |
163 | "FORGOT_PASSWORD_TEMPLATE": "security/forgot_password.html", | |
164 | "LOGIN_USER_TEMPLATE": "security/login_user.html", | |
165 | "REGISTER_USER_TEMPLATE": "security/register_user.html", | |
166 | "RESET_PASSWORD_TEMPLATE": "security/reset_password.html", | |
167 | "CHANGE_PASSWORD_TEMPLATE": "security/change_password.html", | |
168 | "SEND_CONFIRMATION_TEMPLATE": "security/send_confirmation.html", | |
169 | "SEND_LOGIN_TEMPLATE": "security/send_login.html", | |
170 | "VERIFY_TEMPLATE": "security/verify.html", | |
171 | "TWO_FACTOR_VERIFY_CODE_TEMPLATE": "security/two_factor_verify_code.html", | |
172 | "TWO_FACTOR_SETUP_TEMPLATE": "security/two_factor_setup.html", | |
173 | "TWO_FACTOR_VERIFY_PASSWORD_TEMPLATE": "security/two_factor_verify_password.html", | |
174 | "CONFIRMABLE": False, | |
175 | "REGISTERABLE": False, | |
176 | "RECOVERABLE": False, | |
177 | "TRACKABLE": False, | |
178 | "PASSWORDLESS": False, | |
179 | "CHANGEABLE": False, | |
180 | "TWO_FACTOR": False, | |
181 | "SEND_REGISTER_EMAIL": True, | |
182 | "SEND_PASSWORD_CHANGE_EMAIL": True, | |
183 | "SEND_PASSWORD_RESET_EMAIL": True, | |
184 | "SEND_PASSWORD_RESET_NOTICE_EMAIL": True, | |
185 | "LOGIN_WITHIN": "1 days", | |
186 | "TWO_FACTOR_AUTHENTICATOR_VALIDITY": 120, | |
187 | "TWO_FACTOR_MAIL_VALIDITY": 300, | |
188 | "TWO_FACTOR_SMS_VALIDITY": 120, | |
189 | "CONFIRM_EMAIL_WITHIN": "5 days", | |
190 | "RESET_PASSWORD_WITHIN": "5 days", | |
191 | "LOGIN_WITHOUT_CONFIRMATION": False, | |
192 | "AUTO_LOGIN_AFTER_CONFIRM": True, | |
193 | "EMAIL_SENDER": LocalProxy( | |
194 | lambda: current_app.config.get("MAIL_DEFAULT_SENDER", "no-reply@localhost") | |
195 | ), | |
196 | "TWO_FACTOR_RESCUE_MAIL": "no-reply@localhost", | |
197 | "TOKEN_AUTHENTICATION_KEY": "auth_token", | |
198 | "TOKEN_AUTHENTICATION_HEADER": "Authentication-Token", | |
199 | "TOKEN_MAX_AGE": None, | |
200 | "CONFIRM_SALT": "confirm-salt", | |
201 | "RESET_SALT": "reset-salt", | |
202 | "LOGIN_SALT": "login-salt", | |
203 | "CHANGE_SALT": "change-salt", | |
204 | "REMEMBER_SALT": "remember-salt", | |
205 | "DEFAULT_REMEMBER_ME": False, | |
206 | "DEFAULT_HTTP_AUTH_REALM": _("Login Required"), | |
207 | "EMAIL_SUBJECT_REGISTER": _("Welcome"), | |
208 | "EMAIL_SUBJECT_CONFIRM": _("Please confirm your email"), | |
209 | "EMAIL_SUBJECT_PASSWORDLESS": _("Login instructions"), | |
210 | "EMAIL_SUBJECT_PASSWORD_NOTICE": _("Your password has been reset"), | |
211 | "EMAIL_SUBJECT_PASSWORD_CHANGE_NOTICE": _("Your password has been changed"), | |
212 | "EMAIL_SUBJECT_PASSWORD_RESET": _("Password reset instructions"), | |
213 | "EMAIL_PLAINTEXT": True, | |
214 | "EMAIL_HTML": True, | |
215 | "EMAIL_SUBJECT_TWO_FACTOR": _("Two-factor Login"), | |
216 | "EMAIL_SUBJECT_TWO_FACTOR_RESCUE": _("Two-factor Rescue"), | |
217 | "USER_IDENTITY_ATTRIBUTES": ["email"], | |
218 | "USER_IDENTITY_MAPPINGS": [ | |
219 | {"email": uia_email_mapper}, | |
220 | {"us_phone_number": uia_phone_mapper}, | |
221 | ], | |
222 | "PHONE_REGION_DEFAULT": "US", | |
223 | "FRESHNESS": timedelta(hours=24), | |
224 | "FRESHNESS_GRACE_PERIOD": timedelta(hours=1), | |
225 | "HASHING_SCHEMES": ["sha256_crypt", "hex_md5"], | |
226 | "DEPRECATED_HASHING_SCHEMES": ["hex_md5"], | |
227 | "DATETIME_FACTORY": datetime.utcnow, | |
228 | "USE_VERIFY_PASSWORD_CACHE": False, | |
229 | "VERIFY_HASH_CACHE_TTL": 60 * 5, | |
230 | "VERIFY_HASH_CACHE_MAX_SIZE": 500, | |
231 | "TOTP_SECRETS": None, | |
232 | "TOTP_ISSUER": None, | |
233 | "SMS_SERVICE": "Dummy", | |
234 | "SMS_SERVICE_CONFIG": { | |
235 | "ACCOUNT_SID": None, | |
236 | "AUTH_TOKEN": None, | |
237 | "PHONE_NUMBER": None, | |
238 | }, | |
239 | "TWO_FACTOR_REQUIRED": False, | |
240 | "TWO_FACTOR_SECRET": None, # Deprecated - use TOTP_SECRETS | |
241 | "TWO_FACTOR_ENABLED_METHODS": ["email", "authenticator", "sms"], | |
242 | "TWO_FACTOR_URI_SERVICE_NAME": "service_name", # Deprecated - use TOTP_ISSUER | |
243 | "TWO_FACTOR_SMS_SERVICE": "Dummy", # Deprecated - use SMS_SERVICE | |
244 | "TWO_FACTOR_SMS_SERVICE_CONFIG": { # Deprecated - use SMS_SERVICE_CONFIG | |
245 | "ACCOUNT_SID": None, | |
246 | "AUTH_TOKEN": None, | |
247 | "PHONE_NUMBER": None, | |
248 | }, | |
249 | "UNIFIED_SIGNIN": False, | |
250 | "US_SETUP_SALT": "us-setup-salt", | |
251 | "US_SIGNIN_URL": "/us-signin", | |
252 | "US_SIGNIN_SEND_CODE_URL": "/us-signin/send-code", | |
253 | "US_SETUP_URL": "/us-setup", | |
254 | "US_VERIFY_URL": "/us-verify", | |
255 | "US_VERIFY_SEND_CODE_URL": "/us-verify/send-code", | |
256 | "US_VERIFY_LINK_URL": "/us-verify-link", | |
257 | "US_QRCODE_URL": "/us-qrcode", | |
258 | "US_POST_SETUP_VIEW": None, | |
259 | "US_SIGNIN_TEMPLATE": "security/us_signin.html", | |
260 | "US_SETUP_TEMPLATE": "security/us_setup.html", | |
261 | "US_VERIFY_TEMPLATE": "security/us_verify.html", | |
262 | "US_ENABLED_METHODS": ["password", "email", "authenticator", "sms"], | |
263 | "US_MFA_REQUIRED": ["password", "email"], | |
264 | "US_TOKEN_VALIDITY": 120, | |
265 | "US_EMAIL_SUBJECT": _("Verification Code"), | |
266 | "US_SETUP_WITHIN": "30 minutes", | |
267 | "US_SIGNIN_REPLACES_LOGIN": False, | |
268 | "CSRF_PROTECT_MECHANISMS": AUTHN_MECHANISMS, | |
269 | "CSRF_IGNORE_UNAUTH_ENDPOINTS": False, | |
270 | "CSRF_COOKIE": {"key": None}, | |
271 | "CSRF_HEADER": "X-XSRF-Token", | |
272 | "CSRF_COOKIE_REFRESH_EACH_REQUEST": False, | |
273 | "BACKWARDS_COMPAT_UNAUTHN": False, | |
274 | "BACKWARDS_COMPAT_AUTH_TOKEN": False, | |
275 | "BACKWARDS_COMPAT_AUTH_TOKEN_INVALIDATE": False, | |
276 | "JOIN_USER_ROLES": True, | |
277 | } | |
278 | ||
279 | #: Default Flask-Security messages | |
280 | _default_messages = { | |
281 | "API_ERROR": (_("Input not appropriate for requested API"), "error"), | |
282 | "UNAUTHORIZED": (_("You do not have permission to view this resource."), "error"), | |
283 | "UNAUTHENTICATED": ( | |
284 | _("You are not authenticated. Please supply the correct credentials."), | |
285 | "error", | |
286 | ), | |
287 | "REAUTHENTICATION_REQUIRED": ( | |
288 | _("You must re-authenticate to access this endpoint"), | |
289 | "error", | |
290 | ), | |
291 | "CONFIRM_REGISTRATION": ( | |
292 | _("Thank you. Confirmation instructions have been sent to %(email)s."), | |
293 | "success", | |
294 | ), | |
295 | "EMAIL_CONFIRMED": (_("Thank you. Your email has been confirmed."), "success"), | |
296 | "ALREADY_CONFIRMED": (_("Your email has already been confirmed."), "info"), | |
297 | "INVALID_CONFIRMATION_TOKEN": (_("Invalid confirmation token."), "error"), | |
298 | "EMAIL_ALREADY_ASSOCIATED": ( | |
299 | _("%(email)s is already associated with an account."), | |
300 | "error", | |
301 | ), | |
302 | "PASSWORD_MISMATCH": (_("Password does not match"), "error"), | |
303 | "RETYPE_PASSWORD_MISMATCH": (_("Passwords do not match"), "error"), | |
304 | "INVALID_REDIRECT": (_("Redirections outside the domain are forbidden"), "error"), | |
305 | "PASSWORD_RESET_REQUEST": ( | |
306 | _("Instructions to reset your password have been sent to %(email)s."), | |
307 | "info", | |
308 | ), | |
309 | "PASSWORD_RESET_EXPIRED": ( | |
310 | _( | |
311 | "You did not reset your password within %(within)s. " | |
312 | "New instructions have been sent to %(email)s." | |
313 | ), | |
314 | "error", | |
315 | ), | |
316 | "INVALID_RESET_PASSWORD_TOKEN": (_("Invalid reset password token."), "error"), | |
317 | "CONFIRMATION_REQUIRED": (_("Email requires confirmation."), "error"), | |
318 | "CONFIRMATION_REQUEST": ( | |
319 | _("Confirmation instructions have been sent to %(email)s."), | |
320 | "info", | |
321 | ), | |
322 | "CONFIRMATION_EXPIRED": ( | |
323 | _( | |
324 | "You did not confirm your email within %(within)s. " | |
325 | "New instructions to confirm your email have been sent " | |
326 | "to %(email)s." | |
327 | ), | |
328 | "error", | |
329 | ), | |
330 | "LOGIN_EXPIRED": ( | |
331 | _( | |
332 | "You did not login within %(within)s. New instructions to login " | |
333 | "have been sent to %(email)s." | |
334 | ), | |
335 | "error", | |
336 | ), | |
337 | "LOGIN_EMAIL_SENT": ( | |
338 | _("Instructions to login have been sent to %(email)s."), | |
339 | "success", | |
340 | ), | |
341 | "INVALID_LOGIN_TOKEN": (_("Invalid login token."), "error"), | |
342 | "DISABLED_ACCOUNT": (_("Account is disabled."), "error"), | |
343 | "EMAIL_NOT_PROVIDED": (_("Email not provided"), "error"), | |
344 | "INVALID_EMAIL_ADDRESS": (_("Invalid email address"), "error"), | |
345 | "INVALID_CODE": (_("Invalid code"), "error"), | |
346 | "PASSWORD_NOT_PROVIDED": (_("Password not provided"), "error"), | |
347 | "PASSWORD_NOT_SET": (_("No password is set for this user"), "error"), | |
348 | "PASSWORD_INVALID_LENGTH": ( | |
349 | _("Password must be at least %(length)s characters"), | |
350 | "error", | |
351 | ), | |
352 | "PASSWORD_TOO_SIMPLE": (_("Password not complex enough"), "error"), | |
353 | "PASSWORD_BREACHED": (_("Password on breached list"), "error"), | |
354 | "PASSWORD_BREACHED_SITE_ERROR": ( | |
355 | _("Failed to contact breached passwords site"), | |
356 | "error", | |
357 | ), | |
358 | "PHONE_INVALID": (_("Phone number not valid e.g. missing country code"), "error"), | |
359 | "USER_DOES_NOT_EXIST": (_("Specified user does not exist"), "error"), | |
360 | "INVALID_PASSWORD": (_("Invalid password"), "error"), | |
361 | "INVALID_PASSWORD_CODE": (_("Password or code submitted is not valid"), "error"), | |
362 | "PASSWORDLESS_LOGIN_SUCCESSFUL": (_("You have successfully logged in."), "success"), | |
363 | "FORGOT_PASSWORD": (_("Forgot password?"), "info"), | |
364 | "PASSWORD_RESET": ( | |
365 | _( | |
366 | "You successfully reset your password and you have been logged in " | |
367 | "automatically." | |
368 | ), | |
369 | "success", | |
370 | ), | |
371 | "PASSWORD_IS_THE_SAME": ( | |
372 | _("Your new password must be different than your previous password."), | |
373 | "error", | |
374 | ), | |
375 | "PASSWORD_CHANGE": (_("You successfully changed your password."), "success"), | |
376 | "LOGIN": (_("Please log in to access this page."), "info"), | |
377 | "REFRESH": (_("Please reauthenticate to access this page."), "info"), | |
378 | "REAUTHENTICATION_SUCCESSFUL": (_("Reauthentication successful"), "info"), | |
379 | "ANONYMOUS_USER_REQUIRED": ( | |
380 | _("You can only access this endpoint when not logged in."), | |
381 | "error", | |
382 | ), | |
383 | "FAILED_TO_SEND_CODE": (_("Failed to send code. Please try again later"), "error"), | |
384 | "TWO_FACTOR_INVALID_TOKEN": (_("Invalid Token"), "error"), | |
385 | "TWO_FACTOR_LOGIN_SUCCESSFUL": (_("Your token has been confirmed"), "success"), | |
386 | "TWO_FACTOR_CHANGE_METHOD_SUCCESSFUL": ( | |
387 | _("You successfully changed your two-factor method."), | |
388 | "success", | |
389 | ), | |
390 | "TWO_FACTOR_PASSWORD_CONFIRMATION_DONE": ( | |
391 | _("You successfully confirmed password"), | |
392 | "success", | |
393 | ), | |
394 | "TWO_FACTOR_PASSWORD_CONFIRMATION_NEEDED": ( | |
395 | _("Password confirmation is needed in order to access page"), | |
396 | "error", | |
397 | ), | |
398 | "TWO_FACTOR_PERMISSION_DENIED": ( | |
399 | _("You currently do not have permissions to access this page"), | |
400 | "error", | |
401 | ), | |
402 | "TWO_FACTOR_METHOD_NOT_AVAILABLE": (_("Marked method is not valid"), "error"), | |
403 | "TWO_FACTOR_DISABLED": ( | |
404 | _("You successfully disabled two factor authorization."), | |
405 | "success", | |
406 | ), | |
407 | "US_METHOD_NOT_AVAILABLE": (_("Requested method is not valid"), "error"), | |
408 | "US_SETUP_EXPIRED": ( | |
409 | _("Setup must be completed within %(within)s. Please start over."), | |
410 | "error", | |
411 | ), | |
412 | "US_SETUP_SUCCESSFUL": (_("Unified sign in setup successful"), "info"), | |
413 | "US_SPECIFY_IDENTITY": (_("You must specify a valid identity to sign in"), "error"), | |
414 | "USE_CODE": (_("Use this code to sign in: %(code)s."), "info"), | |
415 | } | |
416 | ||
417 | _default_forms = { | |
418 | "login_form": LoginForm, | |
419 | "verify_form": VerifyForm, | |
420 | "confirm_register_form": ConfirmRegisterForm, | |
421 | "register_form": RegisterForm, | |
422 | "forgot_password_form": ForgotPasswordForm, | |
423 | "reset_password_form": ResetPasswordForm, | |
424 | "change_password_form": ChangePasswordForm, | |
425 | "send_confirmation_form": SendConfirmationForm, | |
426 | "passwordless_login_form": PasswordlessLoginForm, | |
427 | "two_factor_verify_code_form": TwoFactorVerifyCodeForm, | |
428 | "two_factor_setup_form": TwoFactorSetupForm, | |
429 | "two_factor_verify_password_form": TwoFactorVerifyPasswordForm, | |
430 | "two_factor_rescue_form": TwoFactorRescueForm, | |
431 | "us_signin_form": UnifiedSigninForm, | |
432 | "us_setup_form": UnifiedSigninSetupForm, | |
433 | "us_setup_validate_form": UnifiedSigninSetupValidateForm, | |
434 | "us_verify_form": UnifiedVerifyForm, | |
435 | } | |
436 | ||
437 | ||
438 | def _user_loader(user_id): | |
439 | """ Try to load based on fs_uniquifier (alternative_id) if available. | |
440 | ||
441 | Note that we don't try, and fall back to the other - primarily because some DBs | |
442 | and drivers (psycopg2) really really hate getting mismatched types during queries. | |
443 | They hate it enough that they abort the 'transaction' and refuse to do anything | |
444 | in the future until the transaction is rolled-back. But we don't really control | |
445 | that and there doesn't seem to be any way to catch the actual offensive query - | |
446 | just next time and forever, things fail. | |
447 | This assumes that if the app has fs_uniquifier, it is non-nullable as we specify | |
448 | so we use that and only that. | |
449 | """ | |
450 | if hasattr(_datastore.user_model, "fs_uniquifier"): | |
451 | selector = dict(fs_uniquifier=str(user_id)) | |
452 | else: | |
453 | selector = dict(id=user_id) | |
454 | user = _security.datastore.find_user(**selector) | |
455 | if user and user.active: | |
456 | return user | |
457 | return None | |
458 | ||
459 | ||
460 | def _request_loader(request): | |
461 | # Short-circuit if we have already been called and verified. | |
462 | # This can happen since Flask-Login will call us (if no session) and our own | |
463 | # decorator @auth_token_required can call us. | |
464 | # N.B. we don't call current_user here since that in fact might try and LOAD | |
465 | # a user - which would call us again. | |
466 | if all(hasattr(_request_ctx_stack.top, k) for k in ["fs_authn_via", "user"]): | |
467 | if _request_ctx_stack.top.fs_authn_via == "token": | |
468 | return _request_ctx_stack.top.user | |
469 | ||
470 | header_key = _security.token_authentication_header | |
471 | args_key = _security.token_authentication_key | |
472 | header_token = request.headers.get(header_key, None) | |
473 | token = request.args.get(args_key, header_token) | |
474 | if request.is_json: | |
475 | data = request.get_json(silent=True) or {} | |
476 | if isinstance(data, dict): | |
477 | token = data.get(args_key, token) | |
478 | ||
479 | use_cache = cv("USE_VERIFY_PASSWORD_CACHE") | |
480 | ||
481 | try: | |
482 | data = _security.remember_token_serializer.loads( | |
483 | token, max_age=_security.token_max_age | |
484 | ) | |
485 | user = _security.datastore.find_user(id=data[0]) | |
486 | if not user.active: | |
487 | user = None | |
488 | except Exception: | |
489 | user = None | |
490 | ||
491 | if not user: | |
492 | return _security.login_manager.anonymous_user() | |
493 | if use_cache: | |
494 | cache = getattr(local_cache, "verify_hash_cache", None) | |
495 | if cache is None: | |
496 | cache = VerifyHashCache() | |
497 | local_cache.verify_hash_cache = cache | |
498 | if cache.has_verify_hash_cache(user): | |
499 | _request_ctx_stack.top.fs_authn_via = "token" | |
500 | return user | |
501 | if user.verify_auth_token(data): | |
502 | _request_ctx_stack.top.fs_authn_via = "token" | |
503 | cache.set_cache(user) | |
504 | return user | |
505 | else: | |
506 | if user.verify_auth_token(data): | |
507 | _request_ctx_stack.top.fs_authn_via = "token" | |
508 | return user | |
509 | ||
510 | return _security.login_manager.anonymous_user() | |
511 | ||
512 | ||
513 | def _identity_loader(): | |
514 | if not isinstance(current_user._get_current_object(), AnonymousUserMixin): | |
515 | identity = Identity(current_user.id) | |
516 | return identity | |
517 | ||
518 | ||
519 | def _on_identity_loaded(sender, identity): | |
520 | if hasattr(current_user, "id"): | |
521 | identity.provides.add(UserNeed(current_user.id)) | |
522 | ||
523 | for role in getattr(current_user, "roles", []): | |
524 | identity.provides.add(RoleNeed(role.name)) | |
525 | for fsperm in role.get_permissions(): | |
526 | identity.provides.add(FsPermNeed(fsperm)) | |
527 | ||
528 | identity.user = current_user | |
529 | ||
530 | ||
531 | def _get_login_manager(app, anonymous_user): | |
532 | lm = LoginManager() | |
533 | lm.anonymous_user = anonymous_user or AnonymousUser | |
534 | lm.localize_callback = localize_callback | |
535 | lm.login_view = "%s.login" % cv("BLUEPRINT_NAME", app=app) | |
536 | lm.user_loader(_user_loader) | |
537 | lm.request_loader(_request_loader) | |
538 | ||
539 | if cv("FLASH_MESSAGES", app=app): | |
540 | lm.login_message, lm.login_message_category = cv("MSG_LOGIN", app=app) | |
541 | lm.needs_refresh_message, lm.needs_refresh_message_category = cv( | |
542 | "MSG_REFRESH", app=app | |
543 | ) | |
544 | else: | |
545 | lm.login_message = None | |
546 | lm.needs_refresh_message = None | |
547 | ||
548 | lm.init_app(app) | |
549 | return lm | |
550 | ||
551 | ||
552 | def _get_principal(app): | |
553 | p = Principal(app, use_sessions=False) | |
554 | p.identity_loader(_identity_loader) | |
555 | return p | |
556 | ||
557 | ||
558 | def _get_pwd_context(app): | |
559 | pw_hash = cv("PASSWORD_HASH", app=app) | |
560 | schemes = cv("PASSWORD_SCHEMES", app=app) | |
561 | deprecated = cv("DEPRECATED_PASSWORD_SCHEMES", app=app) | |
562 | if pw_hash not in schemes: | |
563 | allowed = ", ".join(schemes[:-1]) + " and " + schemes[-1] | |
564 | raise ValueError( | |
565 | "Invalid password hashing scheme %r. Allowed values are %s" | |
566 | % (pw_hash, allowed) | |
567 | ) | |
568 | cc = CryptContext( | |
569 | schemes=schemes, | |
570 | default=pw_hash, | |
571 | deprecated=deprecated, | |
572 | **cv("PASSWORD_HASH_PASSLIB_OPTIONS", app=app) | |
573 | ) | |
574 | return cc | |
575 | ||
576 | ||
577 | def _get_i18n_domain(app): | |
578 | return Domain( | |
579 | dirname=cv("I18N_DIRNAME", app=app), domain=cv("I18N_DOMAIN", app=app) | |
580 | ) | |
581 | ||
582 | ||
583 | def _get_hashing_context(app): | |
584 | schemes = cv("HASHING_SCHEMES", app=app) | |
585 | deprecated = cv("DEPRECATED_HASHING_SCHEMES", app=app) | |
586 | return CryptContext(schemes=schemes, deprecated=deprecated) | |
587 | ||
588 | ||
589 | def _get_serializer(app, name): | |
590 | secret_key = app.config.get("SECRET_KEY") | |
591 | salt = app.config.get("SECURITY_%s_SALT" % name.upper()) | |
592 | return URLSafeTimedSerializer(secret_key=secret_key, salt=salt) | |
593 | ||
594 | ||
595 | def _get_state(app, datastore, anonymous_user=None, **kwargs): | |
596 | for key, value in get_config(app).items(): | |
597 | kwargs[key.lower()] = value | |
598 | ||
599 | kwargs.update( | |
600 | dict( | |
601 | app=app, | |
602 | datastore=datastore, | |
603 | principal=_get_principal(app), | |
604 | pwd_context=_get_pwd_context(app), | |
605 | hashing_context=_get_hashing_context(app), | |
606 | i18n_domain=_get_i18n_domain(app), | |
607 | remember_token_serializer=_get_serializer(app, "remember"), | |
608 | login_serializer=_get_serializer(app, "login"), | |
609 | reset_serializer=_get_serializer(app, "reset"), | |
610 | confirm_serializer=_get_serializer(app, "confirm"), | |
611 | us_setup_serializer=_get_serializer(app, "us_setup"), | |
612 | _context_processors={}, | |
613 | _send_mail_task=None, | |
614 | _send_mail=kwargs.get("send_mail", send_mail), | |
615 | _unauthorized_callback=None, | |
616 | _render_json=default_render_json, | |
617 | _want_json=default_want_json, | |
618 | _unauthn_handler=default_unauthn_handler, | |
619 | _reauthn_handler=default_reauthn_handler, | |
620 | _unauthz_handler=default_unauthz_handler, | |
621 | _password_validator=default_password_validator, | |
622 | ) | |
623 | ) | |
624 | ||
625 | if "login_manager" not in kwargs: | |
626 | kwargs["login_manager"] = _get_login_manager(app, anonymous_user) | |
627 | ||
628 | for key, value in _default_forms.items(): | |
629 | if key not in kwargs or not kwargs[key]: | |
630 | kwargs[key] = value | |
631 | ||
632 | return _SecurityState(**kwargs) | |
633 | ||
634 | ||
635 | def _context_processor(): | |
636 | return dict(url_for_security=url_for_security, security=_security) | |
637 | ||
638 | ||
639 | class RoleMixin(object): | |
640 | """Mixin for `Role` model definitions""" | |
641 | ||
642 | def __eq__(self, other): | |
643 | return self.name == other or self.name == getattr(other, "name", None) | |
644 | ||
645 | def __ne__(self, other): | |
646 | return not self.__eq__(other) | |
647 | ||
648 | def __hash__(self): | |
649 | return hash(self.name) | |
650 | ||
651 | def get_permissions(self): | |
652 | """ | |
653 | Return set of permissions associated with role. | |
654 | ||
655 | Either takes a comma separated string of permissions or | |
656 | an interable of strings if permissions are in their own | |
657 | table. | |
658 | ||
659 | .. versionadded:: 3.3.0 | |
660 | """ | |
661 | if hasattr(self, "permissions") and self.permissions: | |
662 | if isinstance(self.permissions, set): | |
663 | return self.permissions | |
664 | elif isinstance(self.permissions, list): | |
665 | return set(self.permissions) | |
666 | else: | |
667 | # Assume this is a comma separated list | |
668 | return set(self.permissions.split(",")) | |
669 | return set([]) | |
670 | ||
671 | def add_permissions(self, permissions): | |
672 | """ | |
673 | Add one or more permissions to role. | |
674 | ||
675 | :param permissions: a set, list, or single string. | |
676 | ||
677 | Caller must commit to DB. | |
678 | ||
679 | .. versionadded:: 3.3.0 | |
680 | """ | |
681 | if hasattr(self, "permissions"): | |
682 | current_perms = self.get_permissions() | |
683 | if isinstance(permissions, set): | |
684 | perms = permissions | |
685 | elif isinstance(permissions, list): | |
686 | perms = set(permissions) | |
687 | else: | |
688 | perms = {permissions} | |
689 | self.permissions = ",".join(current_perms.union(perms)) | |
690 | else: | |
691 | raise NotImplementedError("Role model doesn't have permissions") | |
692 | ||
693 | def remove_permissions(self, permissions): | |
694 | """ | |
695 | Remove one or more permissions from role. | |
696 | ||
697 | :param permissions: a set, list, or single string. | |
698 | ||
699 | Caller must commit to DB. | |
700 | ||
701 | .. versionadded:: 3.3.0 | |
702 | """ | |
703 | if hasattr(self, "permissions"): | |
704 | current_perms = self.get_permissions() | |
705 | if isinstance(permissions, set): | |
706 | perms = permissions | |
707 | elif isinstance(permissions, list): | |
708 | perms = set(permissions) | |
709 | else: | |
710 | perms = {permissions} | |
711 | self.permissions = ",".join(current_perms.difference(perms)) | |
712 | else: | |
713 | raise NotImplementedError("Role model doesn't have permissions") | |
714 | ||
715 | ||
716 | class UserMixin(BaseUserMixin): | |
717 | """Mixin for `User` model definitions""" | |
718 | ||
719 | def get_id(self): | |
720 | """Returns the user identification attribute. | |
721 | ||
722 | This will be `fs_uniquifier` if that is available, else base class id | |
723 | (which is via Flask-Login and is user.id). | |
724 | ||
725 | .. versionadded:: 3.4.0 | |
726 | """ | |
727 | if hasattr(self, "fs_uniquifier") and self.fs_uniquifier is not None: | |
728 | # Use fs_uniquifier as alternative_id if available and not None | |
729 | alternative_id = str(self.fs_uniquifier) | |
730 | if len(alternative_id) > 0: | |
731 | # Return only if alternative_id is a valid value | |
732 | return alternative_id | |
733 | ||
734 | # Use upstream value if alternative_id is unavailable | |
735 | return BaseUserMixin.get_id(self) | |
736 | ||
737 | @property | |
738 | def is_active(self): | |
739 | """Returns `True` if the user is active.""" | |
740 | return self.active | |
741 | ||
742 | def get_auth_token(self): | |
743 | """Constructs the user's authentication token. | |
744 | ||
745 | This data MUST be securely signed using the ``remember_token_serializer`` | |
746 | """ | |
747 | data = [str(self.id), hash_data(self.password)] | |
748 | if hasattr(self, "fs_uniquifier"): | |
749 | data.append(self.fs_uniquifier) | |
750 | return _security.remember_token_serializer.dumps(data) | |
751 | ||
752 | def verify_auth_token(self, data): | |
753 | """ | |
754 | Perform additional verification of contents of auth token. | |
755 | Prior to this being called the token has been validated (via signing) | |
756 | and has not expired. | |
757 | ||
758 | :param data: the data as formulated by :meth:`get_auth_token` | |
759 | ||
760 | .. versionadded:: 3.3.0 | |
761 | """ | |
762 | if len(data) > 2 and hasattr(self, "fs_uniquifier"): | |
763 | # has uniquifier - use that | |
764 | if data[2] == self.fs_uniquifier: | |
765 | return True | |
766 | # Don't even try old way - if they have defined a uniquifier | |
767 | # we want that to be able to invalidate tokens if changed. | |
768 | return False | |
769 | # Fall back to old and very expensive check | |
770 | if verify_hash(data[1], self.password): | |
771 | return True | |
772 | return False | |
773 | ||
774 | def has_role(self, role): | |
775 | """Returns `True` if the user identifies with the specified role. | |
776 | ||
777 | :param role: A role name or `Role` instance""" | |
778 | if isinstance(role, string_types): | |
779 | return role in (role.name for role in self.roles) | |
780 | else: | |
781 | return role in self.roles | |
782 | ||
783 | def has_permission(self, permission): | |
784 | """ | |
785 | Returns `True` if user has this permission (via a role it has). | |
786 | ||
787 | :param permission: permission string name | |
788 | ||
789 | .. versionadded:: 3.3.0 | |
790 | ||
791 | """ | |
792 | for role in self.roles: | |
793 | if hasattr(role, "permissions"): | |
794 | if permission in role.get_permissions(): | |
795 | return True | |
796 | return False | |
797 | ||
798 | def get_security_payload(self): | |
799 | """Serialize user object as response payload.""" | |
800 | return {"id": str(self.id)} | |
801 | ||
802 | def get_redirect_qparams(self, existing=None): | |
803 | """Return user info that will be added to redirect query params. | |
804 | ||
805 | :param existing: A dict that will be updated. | |
806 | :return: A dict whose keys will be query params and values will be query values. | |
807 | ||
808 | .. versionadded:: 3.2.0 | |
809 | """ | |
810 | if not existing: | |
811 | existing = {} | |
812 | existing.update({"email": self.email}) | |
813 | return existing | |
814 | ||
815 | def verify_and_update_password(self, password): | |
816 | """Returns ``True`` if the password is valid for the specified user. | |
817 | ||
818 | Additionally, the hashed password in the database is updated if the | |
819 | hashing algorithm happens to have changed. | |
820 | ||
821 | N.B. you MUST call DB commit if you are using a session-based datastore | |
822 | (such as SqlAlchemy) since the user instance might have been altered | |
823 | (i.e. ``app.security.datastore.commit()``). | |
824 | This is usually handled in the view. | |
825 | ||
826 | :param password: A plaintext password to verify | |
827 | ||
828 | .. versionadded:: 3.2.0 | |
829 | """ | |
830 | return verify_and_update_password(password, self) | |
831 | ||
832 | def calc_username(self): | |
833 | """ Come up with the best 'username' based on how the app | |
834 | is configured (via :py:data:`SECURITY_USER_IDENTITY_ATTRIBUTES`). | |
835 | Returns the first non-null match (and converts to string). | |
836 | In theory this should NEVER be the empty string unless the user | |
837 | record isn't actually valid. | |
838 | ||
839 | .. versionadded:: 3.4.0 | |
840 | """ | |
841 | cusername = None | |
842 | for attr in get_identity_attributes(): | |
843 | cusername = getattr(self, attr, None) | |
844 | if cusername is not None and len(str(cusername)) > 0: | |
845 | break | |
846 | return str(cusername) if cusername is not None else "" | |
847 | ||
848 | def us_send_security_token(self, method, **kwargs): | |
849 | """ Generate and send the security code for unified sign in. | |
850 | ||
851 | :param method: The method in which the code will be sent | |
852 | :param kwargs: Opaque parameters that are subject to change at any time | |
853 | :return: None if successful, error message if not. | |
854 | ||
855 | This is a wrapper around :meth:`us_send_security_token` | |
856 | that can be overridden to manage any errors. | |
857 | ||
858 | .. versionadded:: 3.4.0 | |
859 | """ | |
860 | try: | |
861 | us_send_security_token(self, method, **kwargs) | |
862 | except Exception: | |
863 | return get_message("FAILED_TO_SEND_CODE")[0] | |
864 | return None | |
865 | ||
866 | def tf_send_security_token(self, method, **kwargs): | |
867 | """ Generate and send the security code for two-factor. | |
868 | ||
869 | :param method: The method in which the code will be sent | |
870 | :param kwargs: Opaque parameters that are subject to change at any time | |
871 | :return: None if successful, error message if not. | |
872 | ||
873 | This is a wrapper around :meth:`tf_send_security_token` | |
874 | that can be overridden to manage any errors. | |
875 | ||
876 | .. versionadded:: 3.4.0 | |
877 | """ | |
878 | try: | |
879 | tf_send_security_token(self, method, **kwargs) | |
880 | except Exception: | |
881 | return get_message("FAILED_TO_SEND_CODE")[0] | |
882 | return None | |
883 | ||
884 | ||
885 | class AnonymousUser(AnonymousUserMixin): | |
886 | """AnonymousUser definition""" | |
887 | ||
888 | def __init__(self): | |
889 | self.roles = ImmutableList() | |
890 | ||
891 | def has_role(self, *args): | |
892 | """Returns `False`""" | |
893 | return False | |
894 | ||
895 | ||
896 | class _SecurityState(object): | |
897 | def __init__(self, **kwargs): | |
898 | for key, value in kwargs.items(): | |
899 | setattr(self, key.lower(), value) | |
900 | ||
901 | def _add_ctx_processor(self, endpoint, fn): | |
902 | group = self._context_processors.setdefault(endpoint, []) | |
903 | fn not in group and group.append(fn) | |
904 | ||
905 | def _run_ctx_processor(self, endpoint): | |
906 | rv = {} | |
907 | for g in [None, endpoint]: | |
908 | for fn in self._context_processors.setdefault(g, []): | |
909 | rv.update(fn()) | |
910 | return rv | |
911 | ||
912 | def context_processor(self, fn): | |
913 | self._add_ctx_processor(None, fn) | |
914 | ||
915 | def forgot_password_context_processor(self, fn): | |
916 | self._add_ctx_processor("forgot_password", fn) | |
917 | ||
918 | def login_context_processor(self, fn): | |
919 | self._add_ctx_processor("login", fn) | |
920 | ||
921 | def register_context_processor(self, fn): | |
922 | self._add_ctx_processor("register", fn) | |
923 | ||
924 | def reset_password_context_processor(self, fn): | |
925 | self._add_ctx_processor("reset_password", fn) | |
926 | ||
927 | def change_password_context_processor(self, fn): | |
928 | self._add_ctx_processor("change_password", fn) | |
929 | ||
930 | def send_confirmation_context_processor(self, fn): | |
931 | self._add_ctx_processor("send_confirmation", fn) | |
932 | ||
933 | def send_login_context_processor(self, fn): | |
934 | self._add_ctx_processor("send_login", fn) | |
935 | ||
936 | def verify_context_processor(self, fn): | |
937 | self._add_ctx_processor("verify", fn) | |
938 | ||
939 | def mail_context_processor(self, fn): | |
940 | self._add_ctx_processor("mail", fn) | |
941 | ||
942 | def tf_verify_password_context_processor(self, fn): | |
943 | self._add_ctx_processor("tf_verify_password", fn) | |
944 | ||
945 | def tf_setup_context_processor(self, fn): | |
946 | self._add_ctx_processor("tf_setup", fn) | |
947 | ||
948 | def tf_token_validation_context_processor(self, fn): | |
949 | self._add_ctx_processor("tf_token_validation", fn) | |
950 | ||
951 | def us_signin_context_processor(self, fn): | |
952 | self._add_ctx_processor("us_signin", fn) | |
953 | ||
954 | def us_setup_context_processor(self, fn): | |
955 | self._add_ctx_processor("us_setup", fn) | |
956 | ||
957 | def us_verify_context_processor(self, fn): | |
958 | self._add_ctx_processor("us_verify", fn) | |
959 | ||
960 | def send_mail_task(self, fn): | |
961 | self._send_mail_task = fn | |
962 | ||
963 | def send_mail(self, fn): | |
964 | self._send_mail = fn | |
965 | ||
966 | def unauthorized_handler(self, fn): | |
967 | warnings.warn( | |
968 | "'unauthorized_handler' has been replaced with" | |
969 | " 'unauthz_handler' and 'unauthn_handler'", | |
970 | DeprecationWarning, | |
971 | ) | |
972 | self._unauthorized_callback = fn | |
973 | ||
974 | def totp_factory(self, tf): | |
975 | self._totp_factory = tf | |
976 | ||
977 | def render_json(self, fn): | |
978 | self._render_json = fn | |
979 | ||
980 | def want_json(self, fn): | |
981 | self._want_json = fn | |
982 | ||
983 | def unauthz_handler(self, cb): | |
984 | self._unauthz_handler = cb | |
985 | ||
986 | def unauthn_handler(self, cb): | |
987 | self._unauthn_handler = cb | |
988 | ||
989 | def reauthn_handler(self, cb): | |
990 | self._reauthn_handler = cb | |
991 | ||
992 | def password_validator(self, cb): | |
993 | self._password_validator = cb | |
994 | ||
995 | ||
996 | class Security(object): | |
997 | """The :class:`Security` class initializes the Flask-Security extension. | |
998 | ||
999 | :param app: The application. | |
1000 | :param datastore: An instance of a user datastore. | |
1001 | :param register_blueprint: to register the Security blueprint or not. | |
1002 | :param login_form: set form for the login view | |
1003 | :param verify_form: set form for re-authentication due to freshness check | |
1004 | :param register_form: set form for the register view when | |
1005 | *SECURITY_CONFIRMABLE* is false | |
1006 | :param confirm_register_form: set form for the register view when | |
1007 | *SECURITY_CONFIRMABLE* is true | |
1008 | :param forgot_password_form: set form for the forgot password view | |
1009 | :param reset_password_form: set form for the reset password view | |
1010 | :param change_password_form: set form for the change password view | |
1011 | :param send_confirmation_form: set form for the send confirmation view | |
1012 | :param passwordless_login_form: set form for the passwordless login view | |
1013 | :param two_factor_setup_form: set form for the 2FA setup view | |
1014 | :param two_factor_verify_code_form: set form the the 2FA verify code view | |
1015 | :param two_factor_rescue_form: set form for the 2FA rescue view | |
1016 | :param two_factor_verify_password_form: set form for the 2FA verify password view | |
1017 | :param us_signin_form: set form for the unified sign in view | |
1018 | :param us_setup_form: set form for the unified sign in setup view | |
1019 | :param us_setup_validate_form: set form for the unified sign in setup validate view | |
1020 | :param us_verify_form: set form for re-authenticating due to freshness check | |
1021 | :param anonymous_user: class to use for anonymous user | |
1022 | :param render_template: function to use to render templates. The default is Flask's | |
1023 | render_template() function. | |
1024 | :param send_mail: function to use to send email. Defaults to :func:`send_mail` | |
1025 | :param json_encoder_cls: Class to use as blueprint.json_encoder. | |
1026 | Defaults to :class:`FsJsonEncoder` | |
1027 | :param totp_cls: Class to use as TOTP factory. Defaults to :class:`Totp` | |
1028 | :param phone_util_cls: Class to use for phone number utilities. | |
1029 | Defaults to :class:`PhoneUtil` | |
1030 | ||
1031 | .. versionadded:: 3.4.0 | |
1032 | ``verify_form`` added as part of freshness/re-authentication | |
1033 | ||
1034 | .. versionadded:: 3.4.0 | |
1035 | ``us_signin_form``, ``us_setup_form``, ``us_setup_validate_form``, and | |
1036 | ``us_verify_form`` added as part of the :ref:`unified-sign-in` feature. | |
1037 | ||
1038 | .. versionadded:: 3.4.0 | |
1039 | ``totp_cls`` added to enable applications to implement replay protection - see | |
1040 | :py:class:`Totp`. | |
1041 | ||
1042 | .. versionadded:: 3.4.0 | |
1043 | ``phone_util_cls`` added to allow different phone number | |
1044 | parsing implementations - see :py:class:`PhoneUtil` | |
1045 | """ | |
1046 | ||
1047 | def __init__(self, app=None, datastore=None, register_blueprint=True, **kwargs): | |
1048 | ||
1049 | self.app = app | |
1050 | self._datastore = datastore | |
1051 | self._register_blueprint = register_blueprint | |
1052 | self._kwargs = kwargs | |
1053 | ||
1054 | self._state = None # set by init_app | |
1055 | if app is not None and datastore is not None: | |
1056 | self._state = self.init_app( | |
1057 | app, datastore, register_blueprint=register_blueprint, **kwargs | |
1058 | ) | |
1059 | ||
1060 | def init_app(self, app, datastore=None, register_blueprint=None, **kwargs): | |
1061 | """Initializes the Flask-Security extension for the specified | |
1062 | application and datastore implementation. | |
1063 | ||
1064 | :param app: The application. | |
1065 | :param datastore: An instance of a user datastore. | |
1066 | :param register_blueprint: to register the Security blueprint or not. | |
1067 | """ | |
1068 | self.app = app | |
1069 | ||
1070 | if datastore is None: | |
1071 | datastore = self._datastore | |
1072 | ||
1073 | if register_blueprint is None: | |
1074 | register_blueprint = self._register_blueprint | |
1075 | ||
1076 | for key, value in self._kwargs.items(): | |
1077 | kwargs.setdefault(key, value) | |
1078 | ||
1079 | if "render_template" not in kwargs: | |
1080 | kwargs.setdefault("render_template", self.render_template) | |
1081 | if "json_encoder_cls" not in kwargs: | |
1082 | kwargs.setdefault("json_encoder_cls", FsJsonEncoder) | |
1083 | if "totp_cls" not in kwargs: | |
1084 | kwargs.setdefault("totp_cls", Totp) | |
1085 | if "phone_util_cls" not in kwargs: | |
1086 | kwargs.setdefault("phone_util_cls", PhoneUtil) | |
1087 | ||
1088 | for key, value in _default_config.items(): | |
1089 | app.config.setdefault("SECURITY_" + key, value) | |
1090 | ||
1091 | for key, value in _default_messages.items(): | |
1092 | app.config.setdefault("SECURITY_MSG_" + key, value) | |
1093 | ||
1094 | identity_loaded.connect_via(app)(_on_identity_loaded) | |
1095 | ||
1096 | self._state = state = _get_state(app, datastore, **kwargs) | |
1097 | ||
1098 | if register_blueprint: | |
1099 | bp = create_blueprint( | |
1100 | app, state, __name__, json_encoder=kwargs["json_encoder_cls"] | |
1101 | ) | |
1102 | app.register_blueprint(bp) | |
1103 | app.context_processor(_context_processor) | |
1104 | ||
1105 | @app.before_first_request | |
1106 | def _register_i18n(): | |
1107 | # N.B. as of jinja 2.9 '_' is always registered | |
1108 | # http://jinja.pocoo.org/docs/2.10/extensions/#i18n-extension | |
1109 | if "_" not in app.jinja_env.globals: | |
1110 | current_app.jinja_env.globals["_"] = state.i18n_domain.gettext | |
1111 | # Register so other packages can reference our translations. | |
1112 | current_app.jinja_env.globals["_fsdomain"] = state.i18n_domain.gettext | |
1113 | ||
1114 | @app.before_first_request | |
1115 | def _csrf_init(): | |
1116 | # various config checks - some of these are opinionated in that there | |
1117 | # could be a reason for some of these combinations - but in general | |
1118 | # they cause strange behavior. | |
1119 | # WTF_CSRF_ENABLED defaults to True if not set in Flask-WTF | |
1120 | if not current_app.config.get("WTF_CSRF_ENABLED", True): | |
1121 | return | |
1122 | csrf = current_app.extensions.get("csrf", None) | |
1123 | ||
1124 | # If they don't want ALL mechanisms protected, then they must | |
1125 | # set WTF_CSRF_CHECK_DEFAULT=False so that our decorators get control. | |
1126 | if cv("CSRF_PROTECT_MECHANISMS") != AUTHN_MECHANISMS: | |
1127 | if not csrf: | |
1128 | # This isn't good. | |
1129 | raise ValueError( | |
1130 | "CSRF_PROTECT_MECHANISMS defined but" | |
1131 | " CsrfProtect not part of application" | |
1132 | ) | |
1133 | if current_app.config.get("WTF_CSRF_CHECK_DEFAULT", True): | |
1134 | raise ValueError( | |
1135 | "WTF_CSRF_CHECK_DEFAULT must be set to False if" | |
1136 | " CSRF_PROTECT_MECHANISMS is set" | |
1137 | ) | |
1138 | # We don't get control unless they turn off WTF_CSRF_CHECK_DEFAULT if | |
1139 | # they have enabled global CSRFProtect. | |
1140 | if ( | |
1141 | cv("CSRF_IGNORE_UNAUTH_ENDPOINTS") | |
1142 | and csrf | |
1143 | and current_app.config.get("WTF_CSRF_CHECK_DEFAULT", False) | |
1144 | ): | |
1145 | raise ValueError( | |
1146 | "To ignore unauth endpoints you must set WTF_CSRF_CHECK_DEFAULT" | |
1147 | " to False" | |
1148 | ) | |
1149 | ||
1150 | csrf_cookie = cv("CSRF_COOKIE") | |
1151 | if csrf_cookie and csrf_cookie["key"] and not csrf: | |
1152 | # Common use case is for cookie value to be used as contents for header | |
1153 | # which is only looked at when CsrfProtect is initialized. | |
1154 | # Yes, this is opinionated - they can always get CSRF token via: | |
1155 | # 'get /login' | |
1156 | raise ValueError( | |
1157 | "CSRF_COOKIE defined however CsrfProtect not part of application" | |
1158 | ) | |
1159 | ||
1160 | if csrf: | |
1161 | csrf.exempt("flask_security.views.logout") | |
1162 | if csrf_cookie and csrf_cookie["key"]: | |
1163 | current_app.after_request(csrf_cookie_handler) | |
1164 | # Add configured header to WTF_CSRF_HEADERS | |
1165 | current_app.config["WTF_CSRF_HEADERS"].append(cv("CSRF_HEADER")) | |
1166 | ||
1167 | @app.before_first_request | |
1168 | def _init_phone_util(): | |
1169 | state._phone_util = state.phone_util_cls() | |
1170 | ||
1171 | app.extensions["security"] = state | |
1172 | ||
1173 | if hasattr(app, "cli"): | |
1174 | from .cli import users, roles | |
1175 | ||
1176 | if state.cli_users_name: | |
1177 | app.cli.add_command(users, state.cli_users_name) | |
1178 | if state.cli_roles_name: | |
1179 | app.cli.add_command(roles, state.cli_roles_name) | |
1180 | ||
1181 | # Migrate from TWO_FACTOR config to generic config. | |
1182 | for newc, oldc in [ | |
1183 | ("SECURITY_SMS_SERVICE", "SECURITY_TWO_FACTOR_SMS_SERVICE"), | |
1184 | ("SECURITY_SMS_SERVICE_CONFIG", "SECURITY_TWO_FACTOR_SMS_SERVICE_CONFIG"), | |
1185 | ("SECURITY_TOTP_SECRETS", "SECURITY_TWO_FACTOR_SECRET"), | |
1186 | ("SECURITY_TOTP_ISSUER", "SECURITY_TWO_FACTOR_URI_SERVICE_NAME"), | |
1187 | ]: | |
1188 | if not app.config.get(newc, None): | |
1189 | app.config[newc] = app.config.get(oldc, None) | |
1190 | ||
1191 | # Two factor configuration checks and setup | |
1192 | multi_factor = False | |
1193 | if cv("UNIFIED_SIGNIN", app=app): | |
1194 | multi_factor = True | |
1195 | if len(cv("US_ENABLED_METHODS", app=app)) < 1: | |
1196 | raise ValueError("Must configure some US_ENABLED_METHODS") | |
1197 | if cv("TWO_FACTOR", app=app): | |
1198 | multi_factor = True | |
1199 | if len(cv("TWO_FACTOR_ENABLED_METHODS", app=app)) < 1: | |
1200 | raise ValueError("Must configure some TWO_FACTOR_ENABLED_METHODS") | |
1201 | ||
1202 | if multi_factor: | |
1203 | self._check_modules("pyqrcode", "TWO_FACTOR or UNIFIED_SIGNIN") | |
1204 | self._check_modules("cryptography", "TWO_FACTOR or UNIFIED_SIGNIN") | |
1205 | ||
1206 | need_sms = ( | |
1207 | cv("UNIFIED_SIGNIN", app=app) | |
1208 | and "sms" in cv("US_ENABLED_METHODS", app=app) | |
1209 | ) or ( | |
1210 | cv("TWO_FACTOR", app=app) | |
1211 | and "sms" in cv("TWO_FACTOR_ENABLED_METHODS", app=app) | |
1212 | ) | |
1213 | if need_sms: | |
1214 | sms_service = cv("SMS_SERVICE", app=app) | |
1215 | if sms_service == "Twilio": # pragma: no cover | |
1216 | self._check_modules("twilio", "SMS") | |
1217 | if state.phone_util_cls == PhoneUtil: | |
1218 | self._check_modules("phonenumbers", "SMS") | |
1219 | ||
1220 | secrets = cv("TOTP_SECRETS", app=app) | |
1221 | issuer = cv("TOTP_ISSUER", app=app) | |
1222 | if not secrets or not issuer: | |
1223 | raise ValueError("Both TOTP_SECRETS and TOTP_ISSUER must be set") | |
1224 | state.totp_factory(state.totp_cls(secrets, issuer)) | |
1225 | ||
1226 | if cv("USE_VERIFY_PASSWORD_CACHE", app=app): | |
1227 | self._check_modules("cachetools", "USE_VERIFY_PASSWORD_CACHE") | |
1228 | ||
1229 | if cv("PASSWORD_COMPLEXITY_CHECKER", app=app) == "zxcvbn": | |
1230 | self._check_modules("zxcvbn", "PASSWORD_COMPLEXITY_CHECKER") | |
1231 | return state | |
1232 | ||
1233 | def _check_modules(self, module, config_name): # pragma: no cover | |
1234 | PY3 = sys.version_info[0] == 3 | |
1235 | if PY3: | |
1236 | from importlib.util import find_spec | |
1237 | ||
1238 | module_exists = find_spec(module) | |
1239 | ||
1240 | else: | |
1241 | import imp | |
1242 | ||
1243 | try: | |
1244 | imp.find_module(module) | |
1245 | module_exists = True | |
1246 | except ImportError: | |
1247 | module_exists = False | |
1248 | ||
1249 | if not module_exists: | |
1250 | raise ValueError("{} is required for {}".format(module, config_name)) | |
1251 | ||
1252 | return module_exists | |
1253 | ||
1254 | def render_template(self, *args, **kwargs): | |
1255 | return render_template(*args, **kwargs) | |
1256 | ||
1257 | def send_mail(self, fn): | |
1258 | """ Function used to send emails. | |
1259 | ||
1260 | :param fn: Function with signature(subject, recipient, template, context) | |
1261 | ||
1262 | See :meth:`send_mail` for details. | |
1263 | ||
1264 | .. versionadded:: 3.1.0 | |
1265 | """ | |
1266 | self._state._send_mail = fn | |
1267 | ||
1268 | def render_json(self, cb): | |
1269 | """ Callback to render response payload as JSON. | |
1270 | ||
1271 | :param cb: Callback function with | |
1272 | signature (payload, code, headers=None, user=None) | |
1273 | ||
1274 | :payload: A dict. Please see the formal API spec for details. | |
1275 | :code: Http status code | |
1276 | :headers: Headers object | |
1277 | :user: the UserDatastore object (or None). Note that this is usually | |
1278 | the same as current_user - but not always. | |
1279 | ||
1280 | The default implementation simply returns:: | |
1281 | ||
1282 | headers["Content-Type"] = "application/json" | |
1283 | payload = dict(meta=dict(code=code), response=payload) | |
1284 | return make_response(jsonify(payload), code, headers) | |
1285 | ||
1286 | .. important:: | |
1287 | Be aware the Flask's ``jsonify`` method will first look to see if a | |
1288 | ``json_encoder`` has been set on the blueprint corresponding to the current | |
1289 | request. If not then it looks for a ``json_encoder`` registered on the app; | |
1290 | and finally uses Flask's default JSONEncoder class. Flask-Security registers | |
1291 | :func:`FsJsonEncoder` as its blueprint json_encoder. | |
1292 | ||
1293 | ||
1294 | This can be used by applications to unify all their JSON API responses. | |
1295 | This is called in a request context and should return a Response or something | |
1296 | Flask can create a Response from. | |
1297 | ||
1298 | .. versionadded:: 3.3.0 | |
1299 | """ | |
1300 | self._state._render_json = cb | |
1301 | ||
1302 | def want_json(self, fn): | |
1303 | """ Function that returns True if response should be JSON (based on the request) | |
1304 | ||
1305 | :param fn: Function with the following signature (request) | |
1306 | ||
1307 | :request: Werkzueg/Flask request | |
1308 | ||
1309 | The default implementation returns True if either the Content-Type is | |
1310 | "application/json" or the best Accept header value is "application/json". | |
1311 | ||
1312 | .. versionadded:: 3.3.0 | |
1313 | """ | |
1314 | self._state._want_json = fn | |
1315 | ||
1316 | def unauthz_handler(self, cb): | |
1317 | """ | |
1318 | Callback for failed authorization. | |
1319 | This is called by the :func:`roles_required`, :func:`roles_accepted`, | |
1320 | :func:`permissions_required`, or :func:`permissions_accepted` | |
1321 | if a role or permission is missing. | |
1322 | ||
1323 | :param cb: Callback function with signature (func, params) | |
1324 | ||
1325 | :func: the decorator function (e.g. roles_required) | |
1326 | :params: list of what (if any) was passed to the decorator. | |
1327 | ||
1328 | Should return a Response or something Flask can create a Response from. | |
1329 | Can raise an exception if it is handled as part of | |
1330 | flask.errorhandler(<exception>) | |
1331 | ||
1332 | With the passed parameters the application could deliver a concise error | |
1333 | message. | |
1334 | ||
1335 | .. versionadded:: 3.3.0 | |
1336 | """ | |
1337 | self._state._unauthz_handler = cb | |
1338 | ||
1339 | def unauthn_handler(self, cb): | |
1340 | """ | |
1341 | Callback for failed authentication. | |
1342 | This is called by :func:`auth_required`, :func:`auth_token_required` | |
1343 | or :func:`http_auth_required` if authentication fails. | |
1344 | ||
1345 | :param cb: Callback function with signature (mechanisms, headers=None) | |
1346 | ||
1347 | :mechanisms: List of which authentication mechanisms were tried | |
1348 | :headers: dict of headers to return | |
1349 | ||
1350 | Should return a Response or something Flask can create a Response from. | |
1351 | Can raise an exception if it is handled as part of | |
1352 | ``flask.errorhandler(<exception>)`` | |
1353 | ||
1354 | The default implementation will return a 401 response if the request was JSON, | |
1355 | otherwise lets ``flask_login.login_manager.unauthorized()`` handle redirects. | |
1356 | ||
1357 | .. versionadded:: 3.3.0 | |
1358 | """ | |
1359 | self._state._unauthn_handler = cb | |
1360 | ||
1361 | def reauthn_handler(self, cb): | |
1362 | """ | |
1363 | Callback when endpoint required a fresh authentication. | |
1364 | This is called by :func:`auth_required`. | |
1365 | ||
1366 | :param cb: Callback function with signature (within, grace) | |
1367 | ||
1368 | :within: timedelta that endpoint required fresh authentication within. | |
1369 | :grace: timedelta of grace period that endpoint allowed. | |
1370 | ||
1371 | Should return a Response or something Flask can create a Response from. | |
1372 | Can raise an exception if it is handled as part of | |
1373 | ``flask.errorhandler(<exception>)`` | |
1374 | ||
1375 | The default implementation will return a 401 response if the request was JSON, | |
1376 | otherwise will redirect to :py:data:`SECURITY_US_VERIFY_URL` | |
1377 | (if :py:data:`SECURITY_UNIFIED_SIGNIN` is enabled) | |
1378 | else to :py:data:`SECURITY_VERIFY_URL`. | |
1379 | If both of those are None it sends an ``abort(401)``. | |
1380 | ||
1381 | See :meth:`flask_security.auth_required` for details about freshness checking. | |
1382 | ||
1383 | .. versionadded:: 3.4.0 | |
1384 | """ | |
1385 | self._state._reauthn_handler = cb | |
1386 | ||
1387 | def password_validator(self, cb): | |
1388 | """ | |
1389 | Callback for validating a user password. | |
1390 | This is called on registration as well as change and reset password. | |
1391 | For registration, ``kwargs`` will be all the form input fields that are | |
1392 | attributes of the user model. | |
1393 | For reset/change, ``kwargs`` will be user=UserModel | |
1394 | ||
1395 | :param cb: Callback function with signature (password, is_register, kwargs) | |
1396 | ||
1397 | :password: desired new plain text password | |
1398 | :is_register: True if called as part of initial registration | |
1399 | :kwargs: user info | |
1400 | ||
1401 | Returns: None if password passes all validations. A list of (localized) messages | |
1402 | if not. | |
1403 | ||
1404 | .. versionadded:: 3.4.0 | |
1405 | Refer to :ref:`pass_validation_topic` for more information. | |
1406 | """ | |
1407 | self._state._password_validator = cb | |
1408 | ||
1409 | def __getattr__(self, name): | |
1410 | return getattr(self._state, name, None) |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.datastore | |
3 | ~~~~~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | This module contains an user datastore classes. | |
6 | ||
7 | :copyright: (c) 2012 by Matt Wright. | |
8 | :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag). | |
9 | :license: MIT, see LICENSE for more details. | |
10 | """ | |
11 | import json | |
12 | import uuid | |
13 | ||
14 | from .utils import config_value, get_identity_attributes, string_types | |
15 | ||
16 | ||
17 | class Datastore(object): | |
18 | def __init__(self, db): | |
19 | self.db = db | |
20 | ||
21 | def commit(self): | |
22 | pass | |
23 | ||
24 | def put(self, model): | |
25 | raise NotImplementedError | |
26 | ||
27 | def delete(self, model): | |
28 | raise NotImplementedError | |
29 | ||
30 | ||
31 | class SQLAlchemyDatastore(Datastore): | |
32 | def commit(self): | |
33 | self.db.session.commit() | |
34 | ||
35 | def put(self, model): | |
36 | self.db.session.add(model) | |
37 | return model | |
38 | ||
39 | def delete(self, model): | |
40 | self.db.session.delete(model) | |
41 | ||
42 | ||
43 | class MongoEngineDatastore(Datastore): | |
44 | def put(self, model): | |
45 | model.save() | |
46 | return model | |
47 | ||
48 | def delete(self, model): | |
49 | model.delete() | |
50 | ||
51 | ||
52 | class PeeweeDatastore(Datastore): | |
53 | def put(self, model): | |
54 | model.save() | |
55 | return model | |
56 | ||
57 | def delete(self, model): | |
58 | model.delete_instance(recursive=True) | |
59 | ||
60 | ||
61 | def with_pony_session(f): | |
62 | from functools import wraps | |
63 | ||
64 | @wraps(f) | |
65 | def decorator(*args, **kwargs): | |
66 | from pony.orm import db_session | |
67 | from pony.orm.core import local | |
68 | from flask import ( | |
69 | after_this_request, | |
70 | current_app, | |
71 | has_app_context, | |
72 | has_request_context, | |
73 | ) | |
74 | from flask.signals import appcontext_popped | |
75 | ||
76 | register = local.db_context_counter == 0 | |
77 | if register and (has_app_context() or has_request_context()): | |
78 | db_session.__enter__() | |
79 | ||
80 | result = f(*args, **kwargs) | |
81 | ||
82 | if register: | |
83 | if has_request_context(): | |
84 | ||
85 | @after_this_request | |
86 | def pop(request): | |
87 | db_session.__exit__() | |
88 | return request | |
89 | ||
90 | elif has_app_context(): | |
91 | ||
92 | @appcontext_popped.connect_via(current_app._get_current_object()) | |
93 | def pop(sender, *args, **kwargs): | |
94 | while local.db_context_counter: | |
95 | db_session.__exit__() | |
96 | ||
97 | else: | |
98 | raise RuntimeError("Needs app or request context") | |
99 | return result | |
100 | ||
101 | return decorator | |
102 | ||
103 | ||
104 | class PonyDatastore(Datastore): | |
105 | def commit(self): | |
106 | self.db.commit() | |
107 | ||
108 | @with_pony_session | |
109 | def put(self, model): | |
110 | return model | |
111 | ||
112 | @with_pony_session | |
113 | def delete(self, model): | |
114 | model.delete() | |
115 | ||
116 | ||
117 | class UserDatastore(object): | |
118 | """Abstracted user datastore. | |
119 | ||
120 | :param user_model: A user model class definition | |
121 | :param role_model: A role model class definition | |
122 | ||
123 | .. important:: | |
124 | For mutating operations, the user/role will be added to the | |
125 | datastore (by calling self.put(<object>). If the datastore is session based | |
126 | (such as for SQLAlchemyDatastore) it is up to caller to actually | |
127 | commit the transaction by calling datastore.commit(). | |
128 | """ | |
129 | ||
130 | def __init__(self, user_model, role_model): | |
131 | self.user_model = user_model | |
132 | self.role_model = role_model | |
133 | ||
134 | def _prepare_role_modify_args(self, user, role): | |
135 | if isinstance(user, string_types): | |
136 | user = self.find_user(email=user) | |
137 | if isinstance(role, string_types): | |
138 | role = self.find_role(role) | |
139 | return user, role | |
140 | ||
141 | def _prepare_create_user_args(self, **kwargs): | |
142 | kwargs.setdefault("active", True) | |
143 | roles = kwargs.get("roles", []) | |
144 | for i, role in enumerate(roles): | |
145 | rn = role.name if isinstance(role, self.role_model) else role | |
146 | # see if the role exists | |
147 | roles[i] = self.find_role(rn) | |
148 | kwargs["roles"] = roles | |
149 | if hasattr(self.user_model, "fs_uniquifier"): | |
150 | kwargs.setdefault("fs_uniquifier", uuid.uuid4().hex) | |
151 | return kwargs | |
152 | ||
153 | def _is_numeric(self, value): | |
154 | try: | |
155 | int(value) | |
156 | except (TypeError, ValueError): | |
157 | return False | |
158 | return True | |
159 | ||
160 | def _is_uuid(self, value): | |
161 | return isinstance(value, uuid.UUID) | |
162 | ||
163 | def get_user(self, id_or_email): | |
164 | """Returns a user matching the specified ID or email address.""" | |
165 | raise NotImplementedError | |
166 | ||
167 | def find_user(self, *args, **kwargs): | |
168 | """Returns a user matching the provided parameters.""" | |
169 | raise NotImplementedError | |
170 | ||
171 | def find_role(self, *args, **kwargs): | |
172 | """Returns a role matching the provided name.""" | |
173 | raise NotImplementedError | |
174 | ||
175 | def add_role_to_user(self, user, role): | |
176 | """Adds a role to a user. | |
177 | ||
178 | :param user: The user to manipulate. Can be an User object or email | |
179 | :param role: The role to add to the user. Can be a Role object or | |
180 | string role name | |
181 | """ | |
182 | user, role = self._prepare_role_modify_args(user, role) | |
183 | if role not in user.roles: | |
184 | user.roles.append(role) | |
185 | self.put(user) | |
186 | return True | |
187 | return False | |
188 | ||
189 | def remove_role_from_user(self, user, role): | |
190 | """Removes a role from a user. | |
191 | ||
192 | :param user: The user to manipulate. Can be an User object or email | |
193 | :param role: The role to remove from the user. Can be a Role object or | |
194 | string role name | |
195 | """ | |
196 | rv = False | |
197 | user, role = self._prepare_role_modify_args(user, role) | |
198 | if role in user.roles: | |
199 | rv = True | |
200 | user.roles.remove(role) | |
201 | self.put(user) | |
202 | return rv | |
203 | ||
204 | def toggle_active(self, user): | |
205 | """Toggles a user's active status. Always returns True.""" | |
206 | user.active = not user.active | |
207 | self.put(user) | |
208 | return True | |
209 | ||
210 | def deactivate_user(self, user): | |
211 | """Deactivates a specified user. Returns `True` if a change was made. | |
212 | ||
213 | This will immediately disallow access to all endpoints that require | |
214 | authentication either via session or tokens. | |
215 | The user will not be able to log in again. | |
216 | ||
217 | :param user: The user to deactivate | |
218 | """ | |
219 | if user.active: | |
220 | user.active = False | |
221 | self.put(user) | |
222 | return True | |
223 | return False | |
224 | ||
225 | def activate_user(self, user): | |
226 | """Activates a specified user. Returns `True` if a change was made. | |
227 | ||
228 | :param user: The user to activate | |
229 | """ | |
230 | if not user.active: | |
231 | user.active = True | |
232 | self.put(user) | |
233 | return True | |
234 | return False | |
235 | ||
236 | def set_uniquifier(self, user, uniquifier=None): | |
237 | """ Set user's authentication token uniquifier. | |
238 | This will immediately render outstanding auth tokens, | |
239 | session cookies and remember cookies invalid. | |
240 | ||
241 | :param user: User to modify | |
242 | :param uniquifier: Unique value - if none then uuid.uuid4().hex is used | |
243 | ||
244 | This method is a no-op if the user model doesn't contain the attribute | |
245 | ``fs_uniquifier`` | |
246 | ||
247 | .. versionadded:: 3.3.0 | |
248 | """ | |
249 | if not hasattr(user, "fs_uniquifier"): | |
250 | return | |
251 | if not uniquifier: | |
252 | uniquifier = uuid.uuid4().hex | |
253 | user.fs_uniquifier = uniquifier | |
254 | self.put(user) | |
255 | ||
256 | def create_role(self, **kwargs): | |
257 | """ | |
258 | Creates and returns a new role from the given parameters. | |
259 | Supported params (depending on RoleModel): | |
260 | ||
261 | :kwparam name: Role name | |
262 | :kwparam permissions: a comma delimited list of permissions, a set or a list. | |
263 | These are user-defined strings that correspond to strings used with | |
264 | @permissions_required() | |
265 | ||
266 | .. versionadded:: 3.3.0 | |
267 | ||
268 | """ | |
269 | ||
270 | # By default we just use raw DB model create - for permissions we want to | |
271 | # be nicer and allow sending in a list or set or comma separated string. | |
272 | if "permissions" in kwargs and hasattr(self.role_model, "permissions"): | |
273 | perms = kwargs["permissions"] | |
274 | if isinstance(perms, list) or isinstance(perms, set): | |
275 | perms = ",".join(perms) | |
276 | elif isinstance(perms, string_types): | |
277 | # squash spaces. | |
278 | perms = ",".join([p.strip() for p in perms.split(",")]) | |
279 | kwargs["permissions"] = perms | |
280 | ||
281 | role = self.role_model(**kwargs) | |
282 | return self.put(role) | |
283 | ||
284 | def find_or_create_role(self, name, **kwargs): | |
285 | """Returns a role matching the given name or creates it with any | |
286 | additionally provided parameters. | |
287 | """ | |
288 | kwargs["name"] = name | |
289 | return self.find_role(name) or self.create_role(**kwargs) | |
290 | ||
291 | def create_user(self, **kwargs): | |
292 | """Creates and returns a new user from the given parameters. | |
293 | ||
294 | :kwparam email: required. | |
295 | :kwparam password: Hashed password. | |
296 | :kwparam roles: list of roles to be added to user. | |
297 | Can be Role objects or strings | |
298 | ||
299 | .. danger:: | |
300 | Be aware that whatever `password` is passed in will | |
301 | be stored directly in the DB. Do NOT pass in a plaintext password! | |
302 | Best practice is to pass in ``hash_password(plaintext_password)``. | |
303 | ||
304 | Furthermore, no validation is done on the password (e.g for minimum length). | |
305 | Best practice is to call | |
306 | ``app.security._password_validator(plaintext_password, True)`` | |
307 | and look for a ``None`` return meaning the password conforms to the | |
308 | configured validations. | |
309 | ||
310 | The new user's ``active`` property will be set to ``True`` | |
311 | unless explicitly set to ``False`` in `kwargs`. | |
312 | """ | |
313 | kwargs = self._prepare_create_user_args(**kwargs) | |
314 | user = self.user_model(**kwargs) | |
315 | return self.put(user) | |
316 | ||
317 | def delete_user(self, user): | |
318 | """Deletes the specified user. | |
319 | ||
320 | :param user: The user to delete | |
321 | """ | |
322 | self.delete(user) | |
323 | ||
324 | def reset_user_access(self, user): | |
325 | """ | |
326 | Use this method to reset user authentication methods in the case of compromise. | |
327 | This will: | |
328 | ||
329 | * reset fs_uniquifier - which causes session cookie, remember cookie, auth | |
330 | tokens to be unusable | |
331 | * remove all unified signin TOTP secrets so those can't be used | |
332 | * remove all two-factor secrets so those can't be used | |
333 | ||
334 | Note that if using unified sign in and allow 'email' as a way to receive a code | |
335 | if the email is compromised - login is still possible. To handle this - it | |
336 | is better to deactivate the user. | |
337 | ||
338 | Note - this method isn't used directly by Flask-Security - it is provided | |
339 | as a helper for an applications administrative needs. | |
340 | ||
341 | Remember to call commit on DB if needed. | |
342 | ||
343 | .. versionadded:: 3.4.1 | |
344 | """ | |
345 | self.set_uniquifier(user) | |
346 | if hasattr(user, "us_totp_secrets"): | |
347 | self.us_reset(user) | |
348 | if hasattr(user, "tf_primary_method"): | |
349 | self.tf_reset(user) | |
350 | ||
351 | def tf_set(self, user, primary_method, totp_secret=None, phone=None): | |
352 | """ Set two-factor info into user record. | |
353 | This carefully only changes things if different. | |
354 | ||
355 | If totp_secret isn't provided - existing one won't be changed. | |
356 | If phone isn't provided, the existing phone number won't be changed. | |
357 | ||
358 | This could be called from an application to apiori setup a user for two factor | |
359 | without the user having to go through the setup process. | |
360 | ||
361 | To get a totp_secret - use ``app.security._totp_factory.generate_totp_secret()`` | |
362 | ||
363 | .. versionadded: 3.4.1 | |
364 | """ | |
365 | ||
366 | changed = False | |
367 | if user.tf_primary_method != primary_method: | |
368 | user.tf_primary_method = primary_method | |
369 | changed = True | |
370 | if totp_secret and user.tf_totp_secret != totp_secret: | |
371 | user.tf_totp_secret = totp_secret | |
372 | changed = True | |
373 | if phone and user.tf_phone_number != phone: | |
374 | user.tf_phone_number = phone | |
375 | changed = True | |
376 | if changed: | |
377 | self.put(user) | |
378 | ||
379 | def tf_reset(self, user): | |
380 | """ Disable two-factor auth for user | |
381 | ||
382 | .. versionadded: 3.4.1 | |
383 | """ | |
384 | user.tf_primary_method = None | |
385 | user.tf_totp_secret = None | |
386 | user.tf_phone_number = None | |
387 | self.put(user) | |
388 | ||
389 | def us_get_totp_secrets(self, user): | |
390 | """ Return totp secrets. | |
391 | These are json encoded in the DB. | |
392 | ||
393 | Returns a dict with methods as keys and secrets as values. | |
394 | ||
395 | .. versionadded:: 3.4.0 | |
396 | """ | |
397 | if not user.us_totp_secrets: | |
398 | return {} | |
399 | return json.loads(user.us_totp_secrets) | |
400 | ||
401 | def us_put_totp_secrets(self, user, secrets): | |
402 | """ Save secrets. Assume to be a dict (or None) | |
403 | with keys as methods, and values as (encrypted) secrets. | |
404 | ||
405 | .. versionadded:: 3.4.0 | |
406 | """ | |
407 | user.us_totp_secrets = json.dumps(secrets) if secrets else None | |
408 | self.put(user) | |
409 | ||
410 | def us_set(self, user, method, totp_secret=None, phone=None): | |
411 | """ Set unified sign in info into user record. | |
412 | ||
413 | If totp_secret isn't provided - existing one won't be changed. | |
414 | If phone isn't provided, the existing phone number won't be changed. | |
415 | ||
416 | This could be called from an application to apiori setup a user for unified | |
417 | sign in without the user having to go through the setup process. | |
418 | ||
419 | To get a totp_secret - use ``app.security._totp_factory.generate_totp_secret()`` | |
420 | ||
421 | .. versionadded: 3.4.1 | |
422 | """ | |
423 | ||
424 | if totp_secret: | |
425 | totp_secrets = self.us_get_totp_secrets(user) | |
426 | totp_secrets[method] = totp_secret | |
427 | self.us_put_totp_secrets(user, totp_secrets) | |
428 | if phone and user.us_phone_number != phone: | |
429 | user.us_phone_number = phone | |
430 | self.put(user) | |
431 | ||
432 | def us_reset(self, user): | |
433 | """ Disable unified sign in for user. | |
434 | Be aware that if "email" is an allowed way to receive codes, they | |
435 | will still work (as totp secrets are generated on the fly). | |
436 | This will disable authenticator app and SMS. | |
437 | ||
438 | .. versionadded: 3.4.1 | |
439 | """ | |
440 | user.us_totp_secrets = None | |
441 | user.us_phone_number = None | |
442 | self.put(user) | |
443 | ||
444 | ||
445 | class SQLAlchemyUserDatastore(SQLAlchemyDatastore, UserDatastore): | |
446 | """A SQLAlchemy datastore implementation for Flask-Security that assumes the | |
447 | use of the Flask-SQLAlchemy extension. | |
448 | """ | |
449 | ||
450 | def __init__(self, db, user_model, role_model): | |
451 | SQLAlchemyDatastore.__init__(self, db) | |
452 | UserDatastore.__init__(self, user_model, role_model) | |
453 | ||
454 | def get_user(self, identifier): | |
455 | from sqlalchemy import func as alchemyFn | |
456 | from sqlalchemy import inspect | |
457 | from sqlalchemy.sql import sqltypes | |
458 | from sqlalchemy.dialects.postgresql import UUID as PSQL_UUID | |
459 | ||
460 | user_model_query = self.user_model.query | |
461 | if config_value("JOIN_USER_ROLES") and hasattr(self.user_model, "roles"): | |
462 | from sqlalchemy.orm import joinedload | |
463 | ||
464 | user_model_query = user_model_query.options(joinedload("roles")) | |
465 | ||
466 | # To support both numeric, string, and UUID primary keys, and support | |
467 | # calling this routine with either a numeric value or a string or a UUID | |
468 | # we need to make sure the types basically match. | |
469 | # psycopg2 for example will complain if we attempt to 'get' a | |
470 | # numeric primary key with a string value. | |
471 | # TODO: other datastores don't support this - they assume the only | |
472 | # PK is user.id. That makes things easier but for backwards compat... | |
473 | ins = inspect(self.user_model) | |
474 | pk_type = ins.primary_key[0].type | |
475 | pk_isnumeric = isinstance(pk_type, sqltypes.Integer) | |
476 | pk_isuuid = isinstance(pk_type, PSQL_UUID) | |
477 | # Are they the same or NOT numeric nor UUID | |
478 | if ( | |
479 | (pk_isnumeric and self._is_numeric(identifier)) | |
480 | or (pk_isuuid and self._is_uuid(identifier)) | |
481 | or (not pk_isnumeric and not pk_isuuid) | |
482 | ): | |
483 | rv = self.user_model.query.get(identifier) | |
484 | if rv is not None: | |
485 | return rv | |
486 | ||
487 | # Not PK - iterate through other attributes and look for 'identifier' | |
488 | for attr in get_identity_attributes(): | |
489 | column = getattr(self.user_model, attr) | |
490 | attr_isnumeric = isinstance(column.type, sqltypes.Integer) | |
491 | ||
492 | query = None | |
493 | if attr_isnumeric and self._is_numeric(identifier): | |
494 | query = column == identifier | |
495 | elif not attr_isnumeric and not self._is_numeric(identifier): | |
496 | # Look for exact case-insensitive match - 'ilike' honors | |
497 | # wild cards which isn't what we want. | |
498 | query = alchemyFn.lower(column) == alchemyFn.lower(identifier) | |
499 | if query is not None: | |
500 | rv = user_model_query.filter(query).first() | |
501 | if rv is not None: | |
502 | return rv | |
503 | ||
504 | def find_user(self, **kwargs): | |
505 | query = self.user_model.query | |
506 | if config_value("JOIN_USER_ROLES") and hasattr(self.user_model, "roles"): | |
507 | from sqlalchemy.orm import joinedload | |
508 | ||
509 | query = query.options(joinedload("roles")) | |
510 | ||
511 | return query.filter_by(**kwargs).first() | |
512 | ||
513 | def find_role(self, role): | |
514 | return self.role_model.query.filter_by(name=role).first() | |
515 | ||
516 | ||
517 | class SQLAlchemySessionUserDatastore(SQLAlchemyUserDatastore, SQLAlchemyDatastore): | |
518 | """A SQLAlchemy datastore implementation for Flask-Security that assumes the | |
519 | use of the flask_sqlalchemy_session extension. | |
520 | """ | |
521 | ||
522 | def __init__(self, session, user_model, role_model): | |
523 | class PretendFlaskSQLAlchemyDb(object): | |
524 | """ This is a pretend db object, so we can just pass in a session. | |
525 | """ | |
526 | ||
527 | def __init__(self, session): | |
528 | self.session = session | |
529 | ||
530 | SQLAlchemyUserDatastore.__init__( | |
531 | self, PretendFlaskSQLAlchemyDb(session), user_model, role_model | |
532 | ) | |
533 | ||
534 | def commit(self): | |
535 | super(SQLAlchemySessionUserDatastore, self).commit() | |
536 | ||
537 | ||
538 | class MongoEngineUserDatastore(MongoEngineDatastore, UserDatastore): | |
539 | """A MongoEngine datastore implementation for Flask-Security that assumes | |
540 | the use of the Flask-MongoEngine extension. | |
541 | """ | |
542 | ||
543 | def __init__(self, db, user_model, role_model): | |
544 | MongoEngineDatastore.__init__(self, db) | |
545 | UserDatastore.__init__(self, user_model, role_model) | |
546 | ||
547 | def get_user(self, identifier): | |
548 | from mongoengine import ValidationError | |
549 | ||
550 | try: | |
551 | return self.user_model.objects(id=identifier).first() | |
552 | except (ValidationError, ValueError): | |
553 | pass | |
554 | ||
555 | is_numeric = self._is_numeric(identifier) | |
556 | ||
557 | for attr in get_identity_attributes(): | |
558 | query_key = attr if is_numeric else "%s__iexact" % attr | |
559 | query = {query_key: identifier} | |
560 | try: | |
561 | rv = self.user_model.objects(**query).first() | |
562 | if rv is not None: | |
563 | return rv | |
564 | except (ValidationError, ValueError): | |
565 | # This can happen if identifier is a string but attribute is | |
566 | # an int. | |
567 | pass | |
568 | ||
569 | def find_user(self, **kwargs): | |
570 | try: | |
571 | from mongoengine.queryset import Q, QCombination | |
572 | except ImportError: | |
573 | from mongoengine.queryset.visitor import Q, QCombination | |
574 | from mongoengine.errors import ValidationError | |
575 | ||
576 | queries = map(lambda i: Q(**{i[0]: i[1]}), kwargs.items()) | |
577 | query = QCombination(QCombination.AND, queries) | |
578 | try: | |
579 | return self.user_model.objects(query).first() | |
580 | except ValidationError: # pragma: no cover | |
581 | return None | |
582 | ||
583 | def find_role(self, role): | |
584 | return self.role_model.objects(name=role).first() | |
585 | ||
586 | ||
587 | class PeeweeUserDatastore(PeeweeDatastore, UserDatastore): | |
588 | """A PeeweeD datastore implementation for Flask-Security that assumes the | |
589 | use of Peewee Flask utils. | |
590 | ||
591 | :param user_model: A user model class definition | |
592 | :param role_model: A role model class definition | |
593 | :param role_link: A model implementing the many-to-many user-role relation | |
594 | """ | |
595 | ||
596 | def __init__(self, db, user_model, role_model, role_link): | |
597 | PeeweeDatastore.__init__(self, db) | |
598 | UserDatastore.__init__(self, user_model, role_model) | |
599 | self.UserRole = role_link | |
600 | ||
601 | def get_user(self, identifier): | |
602 | from peewee import fn as peeweeFn | |
603 | from peewee import IntegerField | |
604 | ||
605 | # For peewee we only (currently) support numeric primary keys. | |
606 | if self._is_numeric(identifier): | |
607 | try: | |
608 | return self.user_model.get(self.user_model.id == identifier) | |
609 | except (self.user_model.DoesNotExist, ValueError): | |
610 | pass | |
611 | ||
612 | for attr in get_identity_attributes(): | |
613 | # Read above (SQLAlchemy store) for why we are checking types. | |
614 | column = getattr(self.user_model, attr) | |
615 | attr_isnumeric = isinstance(column, IntegerField) | |
616 | try: | |
617 | if attr_isnumeric and self._is_numeric(identifier): | |
618 | return self.user_model.get(column == identifier) | |
619 | elif not attr_isnumeric and not self._is_numeric(identifier): | |
620 | return self.user_model.get( | |
621 | peeweeFn.Lower(column) == peeweeFn.Lower(identifier) | |
622 | ) | |
623 | except (self.user_model.DoesNotExist, ValueError): | |
624 | pass | |
625 | ||
626 | def find_user(self, **kwargs): | |
627 | try: | |
628 | return self.user_model.filter(**kwargs).get() | |
629 | except self.user_model.DoesNotExist: | |
630 | return None | |
631 | ||
632 | def find_role(self, role): | |
633 | try: | |
634 | return self.role_model.filter(name=role).get() | |
635 | except self.role_model.DoesNotExist: | |
636 | return None | |
637 | ||
638 | def create_user(self, **kwargs): | |
639 | """Creates and returns a new user from the given parameters.""" | |
640 | roles = kwargs.pop("roles", []) | |
641 | user = self.user_model(**self._prepare_create_user_args(**kwargs)) | |
642 | user = self.put(user) | |
643 | for role in roles: | |
644 | self.add_role_to_user(user, role) | |
645 | self.put(user) | |
646 | return user | |
647 | ||
648 | def add_role_to_user(self, user, role): | |
649 | """Adds a role to a user. | |
650 | ||
651 | :param user: The user to manipulate | |
652 | :param role: The role to add to the user | |
653 | """ | |
654 | user, role = self._prepare_role_modify_args(user, role) | |
655 | result = self.UserRole.select().where( | |
656 | self.UserRole.user == user.id, self.UserRole.role == role.id | |
657 | ) | |
658 | if result.count(): | |
659 | return False | |
660 | else: | |
661 | self.put(self.UserRole.create(user=user.id, role=role.id)) | |
662 | return True | |
663 | ||
664 | def remove_role_from_user(self, user, role): | |
665 | """Removes a role from a user. | |
666 | ||
667 | :param user: The user to manipulate | |
668 | :param role: The role to remove from the user | |
669 | """ | |
670 | user, role = self._prepare_role_modify_args(user, role) | |
671 | result = self.UserRole.select().where( | |
672 | self.UserRole.user == user, self.UserRole.role == role | |
673 | ) | |
674 | if result.count(): | |
675 | query = self.UserRole.delete().where( | |
676 | self.UserRole.user == user, self.UserRole.role == role | |
677 | ) | |
678 | query.execute() | |
679 | return True | |
680 | else: | |
681 | return False | |
682 | ||
683 | ||
684 | class PonyUserDatastore(PonyDatastore, UserDatastore): | |
685 | """A Pony ORM datastore implementation for Flask-Security. | |
686 | ||
687 | Code primarily from https://github.com/ET-CS but taken over after | |
688 | being abandoned. | |
689 | """ | |
690 | ||
691 | def __init__(self, db, user_model, role_model): | |
692 | PonyDatastore.__init__(self, db) | |
693 | UserDatastore.__init__(self, user_model, role_model) | |
694 | ||
695 | @with_pony_session | |
696 | def get_user(self, identifier): | |
697 | from pony.orm.core import ObjectNotFound | |
698 | ||
699 | try: | |
700 | return self.user_model[identifier] | |
701 | except (ObjectNotFound, ValueError): | |
702 | pass | |
703 | ||
704 | for attr in get_identity_attributes(): | |
705 | # this is a nightmare, tl;dr we need to get the thing that | |
706 | # corresponds to email (usually) | |
707 | try: | |
708 | user = self.user_model.get(**{attr: identifier}) | |
709 | if user is not None: | |
710 | return user | |
711 | except (TypeError, ValueError): | |
712 | pass | |
713 | ||
714 | @with_pony_session | |
715 | def find_user(self, **kwargs): | |
716 | return self.user_model.get(**kwargs) | |
717 | ||
718 | @with_pony_session | |
719 | def find_role(self, role): | |
720 | return self.role_model.get(name=role) | |
721 | ||
722 | @with_pony_session | |
723 | def add_role_to_user(self, *args, **kwargs): | |
724 | return super(PonyUserDatastore, self).add_role_to_user(*args, **kwargs) | |
725 | ||
726 | @with_pony_session | |
727 | def create_user(self, **kwargs): | |
728 | return super(PonyUserDatastore, self).create_user(**kwargs) | |
729 | ||
730 | @with_pony_session | |
731 | def create_role(self, **kwargs): | |
732 | return super(PonyUserDatastore, self).create_role(**kwargs) |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.decorators | |
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Flask-Security decorators module | |
6 | ||
7 | :copyright: (c) 2012-2019 by Matt Wright. | |
8 | :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag). | |
9 | :license: MIT, see LICENSE for more details. | |
10 | """ | |
11 | ||
12 | from collections import namedtuple | |
13 | import datetime | |
14 | from functools import wraps | |
15 | ||
16 | from flask import Response, _request_ctx_stack, abort, current_app, g, redirect, request | |
17 | from flask_login import current_user, login_required # noqa: F401 | |
18 | from flask_principal import Identity, Permission, RoleNeed, identity_changed | |
19 | from flask_wtf.csrf import CSRFError | |
20 | from werkzeug.local import LocalProxy | |
21 | from werkzeug.routing import BuildError | |
22 | ||
23 | from .utils import ( | |
24 | FsPermNeed, | |
25 | config_value, | |
26 | do_flash, | |
27 | get_message, | |
28 | get_url, | |
29 | check_and_update_authn_fresh, | |
30 | json_error_response, | |
31 | ) | |
32 | ||
33 | # Convenient references | |
34 | _security = LocalProxy(lambda: current_app.extensions["security"]) | |
35 | ||
36 | _csrf = LocalProxy(lambda: current_app.extensions["csrf"]) | |
37 | ||
38 | BasicAuth = namedtuple("BasicAuth", "username, password") | |
39 | ||
40 | # NOTE: this is here for backwards compatibility, it is deprecated and | |
41 | # to be removed in 4.0 | |
42 | _default_unauthenticated_html = """ | |
43 | <h1>Unauthorized</h1> | |
44 | <p>The server could not verify that you are authorized to access the URL | |
45 | requested. You either supplied the wrong credentials (e.g. a bad password), | |
46 | or your browser doesn't understand how to supply the credentials required. | |
47 | </p> | |
48 | """ | |
49 | ||
50 | ||
51 | def _get_unauthenticated_response(text=None, headers=None): | |
52 | text = text or _default_unauthenticated_html | |
53 | headers = headers or {} | |
54 | return Response(text, 401, headers) | |
55 | ||
56 | ||
57 | def _get_unauthorized_response(text=None, headers=None): # pragma: no cover | |
58 | # People called this - even though it isn't public - no harm in keeping it. | |
59 | return _get_unauthenticated_response(text, headers) | |
60 | ||
61 | ||
62 | def default_unauthn_handler(mechanisms, headers=None): | |
63 | """ Default callback for failures to authenticate | |
64 | ||
65 | If caller wants JSON - return 401 | |
66 | Otherwise - assume caller is html and redirect if possible to a login view. | |
67 | We let Flask-Login handle this. | |
68 | ||
69 | """ | |
70 | msg = get_message("UNAUTHENTICATED")[0] | |
71 | ||
72 | if config_value("BACKWARDS_COMPAT_UNAUTHN"): | |
73 | return _get_unauthenticated_response(headers=headers) | |
74 | if _security._want_json(request): | |
75 | # Ignore headers since today, the only thing in there might be WWW-Authenticate | |
76 | # and we never want to send that in a JSON response (browsers will intercept | |
77 | # that and pop up their own login form). | |
78 | payload = json_error_response(errors=msg) | |
79 | return _security._render_json(payload, 401, None, None) | |
80 | return _security.login_manager.unauthorized() | |
81 | ||
82 | ||
83 | def default_reauthn_handler(within, grace): | |
84 | """ Default callback for 'freshness' related authn failures. | |
85 | ||
86 | If caller wants JSON - return 401 | |
87 | Otherwise - assume caller is html and redirect if possible to configured view. | |
88 | ||
89 | """ | |
90 | m, c = get_message("REAUTHENTICATION_REQUIRED") | |
91 | ||
92 | if _security._want_json(request): | |
93 | is_us = config_value("UNIFIED_SIGNIN") | |
94 | payload = json_error_response(errors=m) | |
95 | payload["reauth_required"] = True | |
96 | payload["unified_signin_enabled"] = is_us | |
97 | return _security._render_json(payload, 401, None, None) | |
98 | ||
99 | if config_value("UNIFIED_SIGNIN"): | |
100 | view = _security.us_verify_url | |
101 | else: | |
102 | view = _security.verify_url | |
103 | if view: | |
104 | do_flash(m, c) | |
105 | redirect_url = get_url(view, qparams={"next": request.url}) | |
106 | return redirect(redirect_url) | |
107 | abort(401) | |
108 | ||
109 | ||
110 | def default_unauthz_handler(func, params): | |
111 | unauthz_message, unauthz_message_type = get_message("UNAUTHORIZED") | |
112 | if _security._want_json(request): | |
113 | payload = json_error_response(errors=unauthz_message) | |
114 | return _security._render_json(payload, 403, None, None) | |
115 | view = config_value("UNAUTHORIZED_VIEW") | |
116 | if view: | |
117 | if callable(view): | |
118 | view = view() | |
119 | else: | |
120 | try: | |
121 | view = get_url(view) | |
122 | except BuildError: | |
123 | view = None | |
124 | do_flash(unauthz_message, unauthz_message_type) | |
125 | redirect_to = "/" | |
126 | if request.referrer and not request.referrer.split("?")[0].endswith( | |
127 | request.path | |
128 | ): | |
129 | redirect_to = request.referrer | |
130 | ||
131 | return redirect(view or redirect_to) | |
132 | abort(403) | |
133 | ||
134 | ||
135 | def _check_token(): | |
136 | # N.B. this isn't great Flask-Login 0.5.0 made this protected | |
137 | # Issue https://github.com/maxcountryman/flask-login/issues/471 | |
138 | # was filed to restore public access. We want to call this via | |
139 | # login_manager in case someone has overridden the login_manager which we | |
140 | # allow. | |
141 | if hasattr(_security.login_manager, "request_callback"): | |
142 | # Pre 0.5.0 | |
143 | user = _security.login_manager.request_callback(request) | |
144 | else: | |
145 | user = _security.login_manager._request_callback(request) | |
146 | ||
147 | if user and user.is_authenticated: | |
148 | app = current_app._get_current_object() | |
149 | _request_ctx_stack.top.user = user | |
150 | identity_changed.send(app, identity=Identity(user.id)) | |
151 | return True | |
152 | ||
153 | return False | |
154 | ||
155 | ||
156 | def _check_http_auth(): | |
157 | auth = request.authorization or BasicAuth(username=None, password=None) | |
158 | if not auth.username: | |
159 | return False | |
160 | user = _security.datastore.get_user(auth.username) | |
161 | ||
162 | if user and user.verify_and_update_password(auth.password): | |
163 | _security.datastore.commit() | |
164 | app = current_app._get_current_object() | |
165 | _request_ctx_stack.top.user = user | |
166 | identity_changed.send(app, identity=Identity(user.id)) | |
167 | return True | |
168 | ||
169 | return False | |
170 | ||
171 | ||
172 | def handle_csrf(method): | |
173 | """ Invoke CSRF protection based on authentication method. | |
174 | ||
175 | Usually this is called as part of a decorator, but if that isn't | |
176 | appropriate, endpoint code can call this directly. | |
177 | ||
178 | If CSRF protection is appropriate, this will call flask_wtf::protect() which | |
179 | will raise a ValidationError on CSRF failure. | |
180 | ||
181 | This routine does nothing if any of these are true: | |
182 | ||
183 | #) *WTF_CSRF_ENABLED* is set to False | |
184 | ||
185 | #) the Flask-WTF CSRF module hasn't been initialized | |
186 | ||
187 | #) csrfProtect already checked and accepted the token | |
188 | ||
189 | If the passed in method is not in *SECURITY_CSRF_PROTECT_MECHANISMS* then not only | |
190 | will no CSRF code be run, but a flag in the current context ``fs_ignore_csrf`` | |
191 | will be set so that downstream code knows to ignore any CSRF checks. | |
192 | ||
193 | .. versionadded:: 3.3.0 | |
194 | """ | |
195 | if ( | |
196 | not current_app.config.get("WTF_CSRF_ENABLED", False) | |
197 | or not current_app.extensions.get("csrf", None) | |
198 | or g.get("csrf_valid", False) | |
199 | ): | |
200 | return | |
201 | ||
202 | if config_value("CSRF_PROTECT_MECHANISMS"): | |
203 | if method in config_value("CSRF_PROTECT_MECHANISMS"): | |
204 | _csrf.protect() | |
205 | else: | |
206 | _request_ctx_stack.top.fs_ignore_csrf = True | |
207 | ||
208 | ||
209 | def http_auth_required(realm): | |
210 | """Decorator that protects endpoints using Basic HTTP authentication. | |
211 | ||
212 | :param realm: optional realm name | |
213 | ||
214 | Once authenticated, if so configured, CSRF protection will be tested. | |
215 | """ | |
216 | ||
217 | def decorator(fn): | |
218 | @wraps(fn) | |
219 | def wrapper(*args, **kwargs): | |
220 | if _check_http_auth(): | |
221 | handle_csrf("basic") | |
222 | return fn(*args, **kwargs) | |
223 | if _security._unauthorized_callback: | |
224 | return _security._unauthorized_callback() | |
225 | else: | |
226 | r = _security.default_http_auth_realm if callable(realm) else realm | |
227 | h = {"WWW-Authenticate": 'Basic realm="%s"' % r} | |
228 | return _security._unauthn_handler(["basic"], headers=h) | |
229 | ||
230 | return wrapper | |
231 | ||
232 | if callable(realm): | |
233 | return decorator(realm) | |
234 | return decorator | |
235 | ||
236 | ||
237 | def auth_token_required(fn): | |
238 | """Decorator that protects endpoints using token authentication. The token | |
239 | should be added to the request by the client by using a query string | |
240 | variable with a name equal to the configuration value of | |
241 | *SECURITY_TOKEN_AUTHENTICATION_KEY* or in a request header named that of | |
242 | the configuration value of *SECURITY_TOKEN_AUTHENTICATION_HEADER* | |
243 | ||
244 | Once authenticated, if so configured, CSRF protection will be tested. | |
245 | """ | |
246 | ||
247 | @wraps(fn) | |
248 | def decorated(*args, **kwargs): | |
249 | if _check_token(): | |
250 | handle_csrf("token") | |
251 | return fn(*args, **kwargs) | |
252 | if _security._unauthorized_callback: | |
253 | return _security._unauthorized_callback() | |
254 | else: | |
255 | return _security._unauthn_handler(["token"]) | |
256 | ||
257 | return decorated | |
258 | ||
259 | ||
260 | def auth_required(*auth_methods, **kwargs): | |
261 | """ | |
262 | Decorator that protects endpoints through multiple mechanisms | |
263 | Example:: | |
264 | ||
265 | @app.route('/dashboard') | |
266 | @auth_required('token', 'session') | |
267 | def dashboard(): | |
268 | return 'Dashboard' | |
269 | ||
270 | :param auth_methods: Specified mechanisms (token, basic, session). If not specified | |
271 | then all current available mechanisms will be tried. | |
272 | :kwparam within: Add 'freshness' check to authentication. Is either an int | |
273 | specifying # of minutes, or a callable that returns a timedelta. For timedeltas, | |
274 | timedelta.total_seconds() is used for the calculations: | |
275 | ||
276 | - If > 0, then the caller must have authenticated within the time specified | |
277 | (as measured using the session cookie). | |
278 | - If 0 and not within the grace period (see below) the caller will | |
279 | always be redirected to re-authenticate. | |
280 | - If < 0 (the default) no freshness check is performed. | |
281 | ||
282 | Note that Basic Auth, by definition, is always 'fresh' and will never result in | |
283 | a redirect/error. | |
284 | :kwparam grace: Add a grace period for freshness checks. As above, either an int | |
285 | or a callable returning a timedelta. If not specified then | |
286 | :py:data:`SECURITY_FRESHNESS_GRACE_PERIOD` is used. The grace period allows | |
287 | callers to complete the required operations w/o being prompted again. | |
288 | See :meth:`flask_security.check_and_update_authn_fresh` for details. | |
289 | ||
290 | Note that regardless of order specified - they will be tried in the following | |
291 | order: token, session, basic. | |
292 | ||
293 | The first mechanism that succeeds is used, following that, depending on | |
294 | configuration, CSRF protection will be tested. | |
295 | ||
296 | On authentication failure `.Security.unauthorized_callback` (deprecated) | |
297 | or :meth:`.Security.unauthn_handler` will be called. | |
298 | ||
299 | .. versionchanged:: 3.3.0 | |
300 | If ``auth_methods`` isn't specified, then all will be tried. Authentication | |
301 | mechanisms will always be tried in order of ``token``, ``session``, ``basic`` | |
302 | regardless of how they are specified in the ``auth_methods`` parameter. | |
303 | ||
304 | .. versionchanged:: 3.4.0 | |
305 | Added ``within`` and ``grace`` parameters to enforce a freshness check. | |
306 | ||
307 | """ | |
308 | ||
309 | login_mechanisms = { | |
310 | "token": lambda: _check_token(), | |
311 | "session": lambda: current_user.is_authenticated, | |
312 | "basic": lambda: _check_http_auth(), | |
313 | } | |
314 | mechanisms_order = ["token", "session", "basic"] | |
315 | if not auth_methods: | |
316 | auth_methods = {"basic", "session", "token"} | |
317 | else: | |
318 | auth_methods = [am for am in auth_methods] | |
319 | ||
320 | def wrapper(fn): | |
321 | @wraps(fn) | |
322 | def decorated_view(*args, **dkwargs): | |
323 | # 2.7 doesn't support keyword args after *args.... | |
324 | within = kwargs.get("within", -1) | |
325 | if callable(within): | |
326 | within = within() | |
327 | else: | |
328 | within = datetime.timedelta(minutes=within) | |
329 | grace = kwargs.get("grace", None) | |
330 | if grace is None: | |
331 | grace = config_value("FRESHNESS_GRACE_PERIOD") | |
332 | elif callable(grace): | |
333 | grace = grace() | |
334 | else: | |
335 | grace = datetime.timedelta(minutes=grace) | |
336 | ||
337 | h = {} | |
338 | if "basic" in auth_methods: | |
339 | r = _security.default_http_auth_realm | |
340 | h["WWW-Authenticate"] = 'Basic realm="%s"' % r | |
341 | mechanisms = [ | |
342 | (method, login_mechanisms.get(method)) | |
343 | for method in mechanisms_order | |
344 | if method in auth_methods | |
345 | ] | |
346 | for method, mechanism in mechanisms: | |
347 | if mechanism and mechanism(): | |
348 | # successfully authenticated. Basic auth is by definition 'fresh'. | |
349 | # Note that using token auth is ok - but caller still has to pass | |
350 | # in a session cookie... | |
351 | if method != "basic" and not check_and_update_authn_fresh( | |
352 | within, grace | |
353 | ): | |
354 | return _security._reauthn_handler(within, grace) | |
355 | handle_csrf(method) | |
356 | return fn(*args, **dkwargs) | |
357 | if _security._unauthorized_callback: | |
358 | return _security._unauthorized_callback() | |
359 | else: | |
360 | return _security._unauthn_handler(auth_methods, headers=h) | |
361 | ||
362 | return decorated_view | |
363 | ||
364 | return wrapper | |
365 | ||
366 | ||
367 | def unauth_csrf(fall_through=False): | |
368 | """Decorator for endpoints that don't need authentication | |
369 | but do want CSRF checks (available via Header rather than just form). | |
370 | This is required when setting *WTF_CSRF_CHECK_DEFAULT* = **False** since in that | |
371 | case, without this decorator, the form validation will attempt to do the CSRF | |
372 | check, and that will fail since the csrf-token is in the header (for pure JSON | |
373 | requests). | |
374 | ||
375 | This decorator does nothing unless Flask-WTF::CSRFProtect has been initialized. | |
376 | ||
377 | This decorator does nothing if *WTF_CSRF_ENABLED* == **False**. | |
378 | ||
379 | This decorator will always require CSRF if the caller is authenticated. | |
380 | ||
381 | This decorator will suppress CSRF if caller isn't authenticated and has set the | |
382 | *SECURITY_CSRF_IGNORE_UNAUTH_ENDPOINTS* config variable. | |
383 | ||
384 | :param fall_through: if set to True, then if CSRF fails here - simply keep going. | |
385 | This is appropriate if underlying view is form based and once the form is | |
386 | instantiated, the csrf_token will be available. | |
387 | Note that this can mask some errors such as 'The CSRF session token is missing.' | |
388 | meaning that the caller didn't send a session cookie and instead the caller | |
389 | might get a 'The CSRF token is missing.' error. | |
390 | ||
391 | .. versionadded:: 3.3.0 | |
392 | """ | |
393 | ||
394 | def wrapper(fn): | |
395 | @wraps(fn) | |
396 | def decorated(*args, **kwargs): | |
397 | if not current_app.config.get( | |
398 | "WTF_CSRF_ENABLED", False | |
399 | ) or not current_app.extensions.get("csrf", None): | |
400 | return fn(*args, **kwargs) | |
401 | ||
402 | if ( | |
403 | config_value("CSRF_IGNORE_UNAUTH_ENDPOINTS") | |
404 | and not current_user.is_authenticated | |
405 | ): | |
406 | _request_ctx_stack.top.fs_ignore_csrf = True | |
407 | else: | |
408 | try: | |
409 | _csrf.protect() | |
410 | except CSRFError: | |
411 | if not fall_through: | |
412 | raise | |
413 | ||
414 | return fn(*args, **kwargs) | |
415 | ||
416 | return decorated | |
417 | ||
418 | return wrapper | |
419 | ||
420 | ||
421 | def roles_required(*roles): | |
422 | """Decorator which specifies that a user must have all the specified roles. | |
423 | Example:: | |
424 | ||
425 | @app.route('/dashboard') | |
426 | @roles_required('admin', 'editor') | |
427 | def dashboard(): | |
428 | return 'Dashboard' | |
429 | ||
430 | The current user must have both the `admin` role and `editor` role in order | |
431 | to view the page. | |
432 | ||
433 | :param roles: The required roles. | |
434 | """ | |
435 | ||
436 | def wrapper(fn): | |
437 | @wraps(fn) | |
438 | def decorated_view(*args, **kwargs): | |
439 | perms = [Permission(RoleNeed(role)) for role in roles] | |
440 | for perm in perms: | |
441 | if not perm.can(): | |
442 | if _security._unauthorized_callback: | |
443 | # Backwards compat - deprecated | |
444 | return _security._unauthorized_callback() | |
445 | return _security._unauthz_handler(roles_required, list(roles)) | |
446 | return fn(*args, **kwargs) | |
447 | ||
448 | return decorated_view | |
449 | ||
450 | return wrapper | |
451 | ||
452 | ||
453 | def roles_accepted(*roles): | |
454 | """Decorator which specifies that a user must have at least one of the | |
455 | specified roles. Example:: | |
456 | ||
457 | @app.route('/create_post') | |
458 | @roles_accepted('editor', 'author') | |
459 | def create_post(): | |
460 | return 'Create Post' | |
461 | ||
462 | The current user must have either the `editor` role or `author` role in | |
463 | order to view the page. | |
464 | ||
465 | :param roles: The possible roles. | |
466 | """ | |
467 | ||
468 | def wrapper(fn): | |
469 | @wraps(fn) | |
470 | def decorated_view(*args, **kwargs): | |
471 | perm = Permission(*[RoleNeed(role) for role in roles]) | |
472 | if perm.can(): | |
473 | return fn(*args, **kwargs) | |
474 | if _security._unauthorized_callback: | |
475 | # Backwards compat - deprecated | |
476 | return _security._unauthorized_callback() | |
477 | return _security._unauthz_handler(roles_accepted, list(roles)) | |
478 | ||
479 | return decorated_view | |
480 | ||
481 | return wrapper | |
482 | ||
483 | ||
484 | def permissions_required(*fsperms): | |
485 | """Decorator which specifies that a user must have all the specified permissions. | |
486 | Example:: | |
487 | ||
488 | @app.route('/dashboard') | |
489 | @permissions_required('admin-write', 'editor-write') | |
490 | def dashboard(): | |
491 | return 'Dashboard' | |
492 | ||
493 | The current user must have BOTH permissions (via the roles it has) | |
494 | to view the page. | |
495 | ||
496 | N.B. Don't confuse these permissions with flask-principle Permission()! | |
497 | ||
498 | :param fsperms: The required permissions. | |
499 | ||
500 | .. versionadded:: 3.3.0 | |
501 | """ | |
502 | ||
503 | def wrapper(fn): | |
504 | @wraps(fn) | |
505 | def decorated_view(*args, **kwargs): | |
506 | perms = [Permission(FsPermNeed(fsperm)) for fsperm in fsperms] | |
507 | for perm in perms: | |
508 | if not perm.can(): | |
509 | if _security._unauthorized_callback: | |
510 | # Backwards compat - deprecated | |
511 | return _security._unauthorized_callback() | |
512 | return _security._unauthz_handler( | |
513 | permissions_required, list(fsperms) | |
514 | ) | |
515 | ||
516 | return fn(*args, **kwargs) | |
517 | ||
518 | return decorated_view | |
519 | ||
520 | return wrapper | |
521 | ||
522 | ||
523 | def permissions_accepted(*fsperms): | |
524 | """Decorator which specifies that a user must have at least one of the | |
525 | specified permissions. Example:: | |
526 | ||
527 | @app.route('/create_post') | |
528 | @permissions_accepted('editor-write', 'author-wrote') | |
529 | def create_post(): | |
530 | return 'Create Post' | |
531 | ||
532 | The current user must have one of the permissions (via the roles it has) | |
533 | to view the page. | |
534 | ||
535 | N.B. Don't confuse these permissions with flask-principle Permission()! | |
536 | ||
537 | :param fsperms: The possible permissions. | |
538 | ||
539 | .. versionadded:: 3.3.0 | |
540 | """ | |
541 | ||
542 | def wrapper(fn): | |
543 | @wraps(fn) | |
544 | def decorated_view(*args, **kwargs): | |
545 | perm = Permission(*[FsPermNeed(fsperm) for fsperm in fsperms]) | |
546 | if perm.can(): | |
547 | return fn(*args, **kwargs) | |
548 | if _security._unauthorized_callback: | |
549 | # Backwards compat - deprecated | |
550 | return _security._unauthorized_callback() | |
551 | return _security._unauthz_handler(permissions_accepted, list(fsperms)) | |
552 | ||
553 | return decorated_view | |
554 | ||
555 | return wrapper | |
556 | ||
557 | ||
558 | def anonymous_user_required(f): | |
559 | """Decorator which requires that caller NOT be logged in. | |
560 | If a logged in user accesses an endpoint protected with this decorator | |
561 | they will be redirected to the *SECURITY_POST_LOGIN_VIEW*. | |
562 | If the caller requests a JSON response, a 400 will be returned. | |
563 | ||
564 | .. versionchanged:: 3.3.0 | |
565 | Support for JSON response was added. | |
566 | """ | |
567 | ||
568 | @wraps(f) | |
569 | def wrapper(*args, **kwargs): | |
570 | if current_user.is_authenticated: | |
571 | if _security._want_json(request): | |
572 | payload = json_error_response( | |
573 | errors=get_message("ANONYMOUS_USER_REQUIRED")[0] | |
574 | ) | |
575 | return _security._render_json(payload, 400, None, None) | |
576 | else: | |
577 | return redirect(get_url(_security.post_login_view)) | |
578 | return f(*args, **kwargs) | |
579 | ||
580 | return wrapper |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.forms | |
3 | ~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Flask-Security forms module | |
6 | ||
7 | :copyright: (c) 2012 by Matt Wright. | |
8 | :copyright: (c) 2017 by CERN. | |
9 | :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag). | |
10 | :license: MIT, see LICENSE for more details. | |
11 | """ | |
12 | ||
13 | import inspect | |
14 | ||
15 | from flask import Markup, current_app, request | |
16 | from flask_login import current_user | |
17 | from flask_wtf import FlaskForm as BaseForm | |
18 | from speaklater import is_lazy_string, make_lazy_string | |
19 | from werkzeug.local import LocalProxy | |
20 | from wtforms import ( | |
21 | BooleanField, | |
22 | Field, | |
23 | HiddenField, | |
24 | PasswordField, | |
25 | RadioField, | |
26 | StringField, | |
27 | SubmitField, | |
28 | ValidationError, | |
29 | validators, | |
30 | ) | |
31 | ||
32 | from .confirmable import requires_confirmation | |
33 | from .utils import ( | |
34 | _, | |
35 | _datastore, | |
36 | config_value, | |
37 | do_flash, | |
38 | get_message, | |
39 | hash_password, | |
40 | localize_callback, | |
41 | url_for_security, | |
42 | validate_redirect_url, | |
43 | ) | |
44 | ||
45 | # Convenient references | |
46 | _security = LocalProxy(lambda: current_app.extensions["security"]) | |
47 | ||
48 | _default_field_labels = { | |
49 | "email": _("Email Address"), | |
50 | "password": _("Password"), | |
51 | "remember_me": _("Remember Me"), | |
52 | "login": _("Login"), | |
53 | "signin": _("Sign In"), | |
54 | "register": _("Register"), | |
55 | "send_confirmation": _("Resend Confirmation Instructions"), | |
56 | "recover_password": _("Recover Password"), | |
57 | "reset_password": _("Reset Password"), | |
58 | "retype_password": _("Retype Password"), | |
59 | "new_password": _("New Password"), | |
60 | "change_password": _("Change Password"), | |
61 | "send_login_link": _("Send Login Link"), | |
62 | "verify_password": _("Verify Password"), | |
63 | "change_method": _("Change Method"), | |
64 | "phone": _("Phone Number"), | |
65 | "code": _("Authentication Code"), | |
66 | "submit": _("Submit"), | |
67 | "submitcode": _("Submit Code"), | |
68 | "error": _("Error(s)"), | |
69 | "identity": _("Identity"), | |
70 | "sendcode": _("Send Code"), | |
71 | "passcode": _("Passcode"), | |
72 | } | |
73 | ||
74 | ||
75 | class ValidatorMixin(object): | |
76 | """ | |
77 | This is called at import time - so there is no app context. | |
78 | Validators have state - namely self.message - but we need that | |
79 | xlated on a per-request basis. So we want a lazy_string - but we can't create | |
80 | that until we are in an app context. | |
81 | """ | |
82 | ||
83 | def __init__(self, *args, **kwargs): | |
84 | # If the message is available from config[MSG_xx] then it can be xlated. | |
85 | # Otherwise it will be used as is. | |
86 | if "message" in kwargs: | |
87 | self._original_message = kwargs["message"] | |
88 | del kwargs["message"] | |
89 | else: | |
90 | self._original_message = None | |
91 | super(ValidatorMixin, self).__init__(*args, **kwargs) | |
92 | ||
93 | def __call__(self, form, field): | |
94 | if self._original_message and ( | |
95 | not is_lazy_string(self.message) and not self.message | |
96 | ): | |
97 | # Creat on first usage within app context. | |
98 | cv = config_value("MSG_" + self._original_message) | |
99 | if cv: | |
100 | self.message = make_lazy_string(_local_xlate, cv[0]) | |
101 | else: | |
102 | self.message = self._original_message | |
103 | return super(ValidatorMixin, self).__call__(form, field) | |
104 | ||
105 | ||
106 | class EqualTo(ValidatorMixin, validators.EqualTo): | |
107 | pass | |
108 | ||
109 | ||
110 | class Required(ValidatorMixin, validators.DataRequired): | |
111 | pass | |
112 | ||
113 | ||
114 | class Email(ValidatorMixin, validators.Email): | |
115 | pass | |
116 | ||
117 | ||
118 | class Length(ValidatorMixin, validators.Length): | |
119 | pass | |
120 | ||
121 | ||
122 | email_required = Required(message="EMAIL_NOT_PROVIDED") | |
123 | email_validator = Email(message="INVALID_EMAIL_ADDRESS") | |
124 | password_required = Required(message="PASSWORD_NOT_PROVIDED") | |
125 | ||
126 | ||
127 | def _local_xlate(text): | |
128 | """ LazyStrings need to be evaluated in the context of a request | |
129 | where _security.i18_domain is available. | |
130 | """ | |
131 | return localize_callback(text) | |
132 | ||
133 | ||
134 | def get_form_field_label(key): | |
135 | """ This is called during import since form fields are declared as part of | |
136 | class. Thus can't call 'localize_callback' until we need to actually | |
137 | translate/render form. | |
138 | """ | |
139 | return make_lazy_string(_local_xlate, _default_field_labels.get(key, "")) | |
140 | ||
141 | ||
142 | def unique_user_email(form, field): | |
143 | if _datastore.get_user(field.data) is not None: | |
144 | msg = get_message("EMAIL_ALREADY_ASSOCIATED", email=field.data)[0] | |
145 | raise ValidationError(msg) | |
146 | ||
147 | ||
148 | def valid_user_email(form, field): | |
149 | form.user = _datastore.get_user(field.data) | |
150 | if form.user is None: | |
151 | raise ValidationError(get_message("USER_DOES_NOT_EXIST")[0]) | |
152 | ||
153 | ||
154 | class Form(BaseForm): | |
155 | def __init__(self, *args, **kwargs): | |
156 | if current_app.testing: | |
157 | self.TIME_LIMIT = None | |
158 | super(Form, self).__init__(*args, **kwargs) | |
159 | ||
160 | ||
161 | class EmailFormMixin: | |
162 | email = StringField( | |
163 | get_form_field_label("email"), validators=[email_required, email_validator] | |
164 | ) | |
165 | ||
166 | ||
167 | class UserEmailFormMixin: | |
168 | user = None | |
169 | email = StringField( | |
170 | get_form_field_label("email"), | |
171 | validators=[email_required, email_validator, valid_user_email], | |
172 | ) | |
173 | ||
174 | ||
175 | class UniqueEmailFormMixin: | |
176 | email = StringField( | |
177 | get_form_field_label("email"), | |
178 | validators=[email_required, email_validator, unique_user_email], | |
179 | ) | |
180 | ||
181 | ||
182 | class PasswordFormMixin: | |
183 | password = PasswordField( | |
184 | get_form_field_label("password"), validators=[password_required] | |
185 | ) | |
186 | ||
187 | ||
188 | class NewPasswordFormMixin: | |
189 | password = PasswordField( | |
190 | get_form_field_label("password"), validators=[password_required] | |
191 | ) | |
192 | ||
193 | ||
194 | class PasswordConfirmFormMixin: | |
195 | password_confirm = PasswordField( | |
196 | get_form_field_label("retype_password"), | |
197 | validators=[ | |
198 | EqualTo("password", message="RETYPE_PASSWORD_MISMATCH"), | |
199 | password_required, | |
200 | ], | |
201 | ) | |
202 | ||
203 | ||
204 | class NextFormMixin: | |
205 | next = HiddenField() | |
206 | ||
207 | def validate_next(self, field): | |
208 | if field.data and not validate_redirect_url(field.data): | |
209 | field.data = "" | |
210 | do_flash(*get_message("INVALID_REDIRECT")) | |
211 | raise ValidationError(get_message("INVALID_REDIRECT")[0]) | |
212 | ||
213 | ||
214 | class RegisterFormMixin: | |
215 | submit = SubmitField(get_form_field_label("register")) | |
216 | ||
217 | def to_dict(self, only_user): | |
218 | """ | |
219 | Return form data as dictionary | |
220 | :param only_user: bool, if True then only fields that have | |
221 | corresponding members in UserModel are returned | |
222 | :return: dict | |
223 | """ | |
224 | ||
225 | def is_field_and_user_attr(member): | |
226 | if not isinstance(member, Field): | |
227 | return False | |
228 | ||
229 | # If only fields recorded on UserModel should be returned, | |
230 | # perform check on user model, else return True | |
231 | if only_user is True: | |
232 | return hasattr(_datastore.user_model, member.name) | |
233 | else: | |
234 | return True | |
235 | ||
236 | fields = inspect.getmembers(self, is_field_and_user_attr) | |
237 | return dict((key, value.data) for key, value in fields) | |
238 | ||
239 | ||
240 | class SendConfirmationForm(Form, UserEmailFormMixin): | |
241 | """The default send confirmation form""" | |
242 | ||
243 | submit = SubmitField(get_form_field_label("send_confirmation")) | |
244 | ||
245 | def __init__(self, *args, **kwargs): | |
246 | super(SendConfirmationForm, self).__init__(*args, **kwargs) | |
247 | if request.method == "GET": | |
248 | self.email.data = request.args.get("email", None) | |
249 | ||
250 | def validate(self): | |
251 | if not super(SendConfirmationForm, self).validate(): | |
252 | return False | |
253 | if self.user.confirmed_at is not None: | |
254 | self.email.errors.append(get_message("ALREADY_CONFIRMED")[0]) | |
255 | return False | |
256 | return True | |
257 | ||
258 | ||
259 | class ForgotPasswordForm(Form, UserEmailFormMixin): | |
260 | """The default forgot password form""" | |
261 | ||
262 | submit = SubmitField(get_form_field_label("recover_password")) | |
263 | ||
264 | def validate(self): | |
265 | if not super(ForgotPasswordForm, self).validate(): | |
266 | return False | |
267 | if not self.user.is_active: | |
268 | self.email.errors.append(get_message("DISABLED_ACCOUNT")[0]) | |
269 | return False | |
270 | if requires_confirmation(self.user): | |
271 | self.email.errors.append(get_message("CONFIRMATION_REQUIRED")[0]) | |
272 | return False | |
273 | return True | |
274 | ||
275 | ||
276 | class PasswordlessLoginForm(Form, UserEmailFormMixin): | |
277 | """The passwordless login form""" | |
278 | ||
279 | submit = SubmitField(get_form_field_label("send_login_link")) | |
280 | ||
281 | def __init__(self, *args, **kwargs): | |
282 | super(PasswordlessLoginForm, self).__init__(*args, **kwargs) | |
283 | ||
284 | def validate(self): | |
285 | if not super(PasswordlessLoginForm, self).validate(): | |
286 | return False | |
287 | if not self.user.is_active: | |
288 | self.email.errors.append(get_message("DISABLED_ACCOUNT")[0]) | |
289 | return False | |
290 | return True | |
291 | ||
292 | ||
293 | class LoginForm(Form, NextFormMixin): | |
294 | """The default login form""" | |
295 | ||
296 | email = StringField(get_form_field_label("email"), validators=[email_required]) | |
297 | password = PasswordField( | |
298 | get_form_field_label("password"), validators=[password_required] | |
299 | ) | |
300 | remember = BooleanField(get_form_field_label("remember_me")) | |
301 | submit = SubmitField(get_form_field_label("login")) | |
302 | ||
303 | def __init__(self, *args, **kwargs): | |
304 | super(LoginForm, self).__init__(*args, **kwargs) | |
305 | if not self.next.data: | |
306 | self.next.data = request.args.get("next", "") | |
307 | self.remember.default = config_value("DEFAULT_REMEMBER_ME") | |
308 | if ( | |
309 | current_app.extensions["security"].recoverable | |
310 | and not self.password.description | |
311 | ): | |
312 | html = Markup( | |
313 | u'<a href="{url}">{message}</a>'.format( | |
314 | url=url_for_security("forgot_password"), | |
315 | message=get_message("FORGOT_PASSWORD")[0], | |
316 | ) | |
317 | ) | |
318 | self.password.description = html | |
319 | ||
320 | def validate(self): | |
321 | if not super(LoginForm, self).validate(): | |
322 | return False | |
323 | ||
324 | self.user = _datastore.get_user(self.email.data) | |
325 | ||
326 | if self.user is None: | |
327 | self.email.errors.append(get_message("USER_DOES_NOT_EXIST")[0]) | |
328 | # Reduce timing variation between existing and non-existing users | |
329 | hash_password(self.password.data) | |
330 | return False | |
331 | if not self.user.password: | |
332 | self.password.errors.append(get_message("PASSWORD_NOT_SET")[0]) | |
333 | # Reduce timing variation between existing and non-existing users | |
334 | hash_password(self.password.data) | |
335 | return False | |
336 | if not self.user.verify_and_update_password(self.password.data): | |
337 | self.password.errors.append(get_message("INVALID_PASSWORD")[0]) | |
338 | return False | |
339 | if requires_confirmation(self.user): | |
340 | self.email.errors.append(get_message("CONFIRMATION_REQUIRED")[0]) | |
341 | return False | |
342 | if not self.user.is_active: | |
343 | self.email.errors.append(get_message("DISABLED_ACCOUNT")[0]) | |
344 | return False | |
345 | return True | |
346 | ||
347 | ||
348 | class VerifyForm(Form, PasswordFormMixin): | |
349 | """The verify authentication form""" | |
350 | ||
351 | user = None | |
352 | submit = SubmitField(get_form_field_label("verify_password")) | |
353 | ||
354 | def validate(self): | |
355 | if not super(VerifyForm, self).validate(): | |
356 | return False | |
357 | ||
358 | self.user = current_user | |
359 | if not self.user.verify_and_update_password(self.password.data): | |
360 | self.password.errors.append(get_message("INVALID_PASSWORD")[0]) | |
361 | return False | |
362 | return True | |
363 | ||
364 | ||
365 | class ConfirmRegisterForm(Form, RegisterFormMixin, UniqueEmailFormMixin): | |
366 | """ This form is used for registering when 'confirmable' is set. | |
367 | The only difference between this and the other RegisterForm is that | |
368 | this one doesn't require re-typing in the password... | |
369 | """ | |
370 | ||
371 | # Password optional when Unified Signin enabled. | |
372 | password = PasswordField( | |
373 | get_form_field_label("password"), validators=[validators.Optional()] | |
374 | ) | |
375 | ||
376 | def validate(self): | |
377 | if not super(ConfirmRegisterForm, self).validate(): | |
378 | return False | |
379 | ||
380 | # To support unified sign in - we permit registering with no password. | |
381 | if not config_value("UNIFIED_SIGNIN"): | |
382 | # password required | |
383 | if not self.password.data or not self.password.data.strip(): | |
384 | self.password.errors.append(get_message("PASSWORD_NOT_PROVIDED")[0]) | |
385 | return False | |
386 | ||
387 | if self.password.data: | |
388 | # We do explicit validation here for passwords | |
389 | # (rather than write a validator class) for 2 reasons: | |
390 | # 1) We want to control which fields are passed - | |
391 | # sometimes that's current_user | |
392 | # other times it's the registration fields. | |
393 | # 2) We want to be able to return multiple error messages. | |
394 | rfields = {} | |
395 | for k, v in self.data.items(): | |
396 | if hasattr(_datastore.user_model, k): | |
397 | rfields[k] = v | |
398 | del rfields["password"] | |
399 | pbad = _security._password_validator(self.password.data, True, **rfields) | |
400 | if pbad: | |
401 | self.password.errors.extend(pbad) | |
402 | return False | |
403 | return True | |
404 | ||
405 | ||
406 | class RegisterForm(ConfirmRegisterForm, NextFormMixin): | |
407 | ||
408 | # Password optional when Unified Signin enabled. | |
409 | password_confirm = PasswordField( | |
410 | get_form_field_label("retype_password"), | |
411 | validators=[ | |
412 | EqualTo("password", message="RETYPE_PASSWORD_MISMATCH"), | |
413 | validators.Optional(), | |
414 | ], | |
415 | ) | |
416 | ||
417 | def validate(self): | |
418 | if not super(RegisterForm, self).validate(): | |
419 | return False | |
420 | if not config_value("UNIFIED_SIGNIN"): | |
421 | # password_confirm required | |
422 | if not self.password_confirm.data or not self.password_confirm.data.strip(): | |
423 | self.password_confirm.errors.append( | |
424 | get_message("PASSWORD_NOT_PROVIDED")[0] | |
425 | ) | |
426 | return False | |
427 | return True | |
428 | ||
429 | def __init__(self, *args, **kwargs): | |
430 | super(RegisterForm, self).__init__(*args, **kwargs) | |
431 | if not self.next.data: | |
432 | self.next.data = request.args.get("next", "") | |
433 | ||
434 | ||
435 | class ResetPasswordForm(Form, NewPasswordFormMixin, PasswordConfirmFormMixin): | |
436 | """The default reset password form""" | |
437 | ||
438 | submit = SubmitField(get_form_field_label("reset_password")) | |
439 | ||
440 | def validate(self): | |
441 | if not super(ResetPasswordForm, self).validate(): | |
442 | return False | |
443 | ||
444 | pbad = _security._password_validator( | |
445 | self.password.data, False, user=current_user | |
446 | ) | |
447 | if pbad: | |
448 | self.password.errors.extend(pbad) | |
449 | return False | |
450 | return True | |
451 | ||
452 | ||
453 | class ChangePasswordForm(Form, PasswordFormMixin): | |
454 | """The default change password form""" | |
455 | ||
456 | new_password = PasswordField( | |
457 | get_form_field_label("new_password"), validators=[password_required] | |
458 | ) | |
459 | ||
460 | new_password_confirm = PasswordField( | |
461 | get_form_field_label("retype_password"), | |
462 | validators=[ | |
463 | EqualTo("new_password", message="RETYPE_PASSWORD_MISMATCH"), | |
464 | password_required, | |
465 | ], | |
466 | ) | |
467 | ||
468 | submit = SubmitField(get_form_field_label("change_password")) | |
469 | ||
470 | def validate(self): | |
471 | if not super(ChangePasswordForm, self).validate(): | |
472 | return False | |
473 | ||
474 | if not current_user.verify_and_update_password(self.password.data): | |
475 | self.password.errors.append(get_message("INVALID_PASSWORD")[0]) | |
476 | return False | |
477 | if self.password.data == self.new_password.data: | |
478 | self.password.errors.append(get_message("PASSWORD_IS_THE_SAME")[0]) | |
479 | return False | |
480 | pbad = _security._password_validator( | |
481 | self.new_password.data, False, user=current_user | |
482 | ) | |
483 | if pbad: | |
484 | self.new_password.errors.extend(pbad) | |
485 | return False | |
486 | return True | |
487 | ||
488 | ||
489 | class TwoFactorSetupForm(Form, UserEmailFormMixin): | |
490 | """The Two-factor token validation form""" | |
491 | ||
492 | setup = RadioField( | |
493 | "Available Methods", | |
494 | choices=[ | |
495 | ("email", "Set up using email"), | |
496 | ( | |
497 | "authenticator", | |
498 | "Set up using an authenticator app (e.g. google, lastpass, authy)", | |
499 | ), | |
500 | ("sms", "Set up using SMS"), | |
501 | ("disable", "Disable two factor authentication"), | |
502 | ], | |
503 | ) | |
504 | phone = StringField(get_form_field_label("phone")) | |
505 | submit = SubmitField(get_form_field_label("submit")) | |
506 | ||
507 | def __init__(self, *args, **kwargs): | |
508 | super(TwoFactorSetupForm, self).__init__(*args, **kwargs) | |
509 | ||
510 | def validate(self): | |
511 | # TODO: the super class validate is never called - thus we have to | |
512 | # initialize errors to lists below. It also means that 'email' is never | |
513 | # validated - though it isn't required so the mixin might not be correct. | |
514 | choices = config_value("TWO_FACTOR_ENABLED_METHODS") | |
515 | if "email" in choices: | |
516 | # backwards compat | |
517 | choices.append("mail") | |
518 | if not config_value("TWO_FACTOR_REQUIRED"): | |
519 | choices.append("disable") | |
520 | if "setup" not in self.data or self.data["setup"] not in choices: | |
521 | self.setup.errors = list() | |
522 | self.setup.errors.append(get_message("TWO_FACTOR_METHOD_NOT_AVAILABLE")[0]) | |
523 | return False | |
524 | if self.setup.data == "sms" and len(self.phone.data) > 0: | |
525 | # Somewhat bizarre - but this isn't required the first time around | |
526 | # when they select "sms". Then they get a field to fill out with | |
527 | # phone number, then Submit again. | |
528 | msg = _security._phone_util.validate_phone_number(self.phone.data) | |
529 | if msg: | |
530 | self.phone.errors = list() | |
531 | self.phone.errors.append(msg) | |
532 | return False | |
533 | ||
534 | return True | |
535 | ||
536 | ||
537 | class TwoFactorVerifyCodeForm(Form, UserEmailFormMixin): | |
538 | """The Two-factor token validation form""" | |
539 | ||
540 | code = StringField(get_form_field_label("code")) | |
541 | submit = SubmitField(get_form_field_label("submitcode")) | |
542 | ||
543 | def __init__(self, *args, **kwargs): | |
544 | super(TwoFactorVerifyCodeForm, self).__init__(*args, **kwargs) | |
545 | ||
546 | def validate(self): | |
547 | # codes sent by sms or mail will be valid for another window cycle | |
548 | if ( | |
549 | self.primary_method == "google_authenticator" | |
550 | or self.primary_method == "authenticator" | |
551 | ): | |
552 | self.window = config_value("TWO_FACTOR_AUTHENTICATOR_VALIDITY") | |
553 | elif self.primary_method == "email" or self.primary_method == "mail": | |
554 | self.window = config_value("TWO_FACTOR_MAIL_VALIDITY") | |
555 | elif self.primary_method == "sms": | |
556 | self.window = config_value("TWO_FACTOR_SMS_VALIDITY") | |
557 | else: | |
558 | return False | |
559 | ||
560 | # verify entered token with user's totp secret | |
561 | if not _security._totp_factory.verify_totp( | |
562 | token=self.code.data, | |
563 | totp_secret=self.tf_totp_secret, | |
564 | user=self.user, | |
565 | window=self.window, | |
566 | ): | |
567 | self.code.errors = list() | |
568 | self.code.errors.append(get_message("TWO_FACTOR_INVALID_TOKEN")[0]) | |
569 | ||
570 | return False | |
571 | ||
572 | return True | |
573 | ||
574 | ||
575 | class TwoFactorVerifyPasswordForm(Form, PasswordFormMixin): | |
576 | """The verify password form""" | |
577 | ||
578 | submit = SubmitField(get_form_field_label("verify_password")) | |
579 | ||
580 | def validate(self): | |
581 | if not super(TwoFactorVerifyPasswordForm, self).validate(): | |
582 | return False | |
583 | ||
584 | self.user = current_user | |
585 | if not self.user.verify_and_update_password(self.password.data): | |
586 | self.password.errors.append(get_message("INVALID_PASSWORD")[0]) | |
587 | return False | |
588 | ||
589 | return True | |
590 | ||
591 | ||
592 | class TwoFactorRescueForm(Form): | |
593 | """The Two-factor Rescue validation form """ | |
594 | ||
595 | help_setup = RadioField( | |
596 | "Trouble Accessing Your Account?", | |
597 | choices=[ | |
598 | ("lost_device", "Can not access mobile device?"), | |
599 | ("no_mail_access", "Can not access mail account?"), | |
600 | ], | |
601 | ) | |
602 | submit = SubmitField(get_form_field_label("submit")) | |
603 | ||
604 | def __init__(self, *args, **kwargs): | |
605 | super(TwoFactorRescueForm, self).__init__(*args, **kwargs) | |
606 | ||
607 | def validate(self): | |
608 | if not super(TwoFactorRescueForm, self).validate(): | |
609 | return False | |
610 | return True |
0 | """" | |
1 | Copyright 2019 by J. Christopher Wagner (jwag). All rights reserved. | |
2 | :license: MIT, see LICENSE for more details. | |
3 | ||
4 | This packages contains OPTIONAL models for various ORMs/databases that can be used | |
5 | to quickly get the required DB models setup. | |
6 | ||
7 | These models have the fields for ALL features. This makes it easy for applications | |
8 | to add features w/o a DB migration (and modern DBs are pretty efficient at storing | |
9 | empty values!). | |
10 | ||
11 | """ |
0 | """ | |
1 | Copyright 2019 by J. Christopher Wagner (jwag). All rights reserved. | |
2 | :license: MIT, see LICENSE for more details. | |
3 | ||
4 | ||
5 | Complete models for all features when using Flask-SqlAlchemy | |
6 | ||
7 | BE AWARE: Once any version of this is shipped no changes can be made - instead | |
8 | a new version needs to be created. | |
9 | """ | |
10 | ||
11 | import datetime | |
12 | from sqlalchemy import ( | |
13 | Boolean, | |
14 | DateTime, | |
15 | Column, | |
16 | Integer, | |
17 | String, | |
18 | UnicodeText, | |
19 | ForeignKey, | |
20 | ) | |
21 | from sqlalchemy.ext.declarative import declared_attr | |
22 | from sqlalchemy.orm import relationship | |
23 | from sqlalchemy.sql import func | |
24 | ||
25 | from flask_security import RoleMixin, UserMixin | |
26 | ||
27 | ||
28 | class FsModels(object): | |
29 | """ | |
30 | Helper class for model mixins. | |
31 | This records the ``db`` (which is a Flask-SqlAlchemy object) for use in | |
32 | mixins. | |
33 | """ | |
34 | ||
35 | roles_users = None | |
36 | db = None | |
37 | fs_model_version = 1 | |
38 | ||
39 | @classmethod | |
40 | def set_db_info(cls, appdb): | |
41 | """ Initialize Model. | |
42 | This needs to be called after the DB object has been created | |
43 | (e.g. db = Sqlalchemy()) | |
44 | """ | |
45 | cls.db = appdb | |
46 | cls.roles_users = appdb.Table( | |
47 | "roles_users", | |
48 | Column("user_id", Integer(), ForeignKey("user.id")), | |
49 | Column("role_id", Integer(), ForeignKey("role.id")), | |
50 | ) | |
51 | ||
52 | ||
53 | class FsRoleMixin(RoleMixin): | |
54 | id = Column(Integer(), primary_key=True) | |
55 | name = Column(String(80), unique=True, nullable=False) | |
56 | description = Column(String(255)) | |
57 | # A comma separated list of strings | |
58 | permissions = Column(UnicodeText, nullable=True) | |
59 | update_datetime = Column( | |
60 | DateTime, | |
61 | nullable=False, | |
62 | server_default=func.now(), | |
63 | onupdate=datetime.datetime.utcnow, | |
64 | ) | |
65 | ||
66 | ||
67 | class FsUserMixin(UserMixin): | |
68 | """ User information | |
69 | """ | |
70 | ||
71 | # flask_security basic fields | |
72 | id = Column(Integer, primary_key=True) | |
73 | email = Column(String(255), unique=True, nullable=False) | |
74 | # Username is important since shouldn't expose email to other users in most cases. | |
75 | username = Column(String(255)) | |
76 | password = Column(String(255), nullable=False) | |
77 | active = Column(Boolean(), nullable=False) | |
78 | ||
79 | # Faster token checking | |
80 | fs_uniquifier = Column(String(64), unique=True, nullable=False) | |
81 | ||
82 | # confirmable | |
83 | confirmed_at = Column(DateTime()) | |
84 | ||
85 | # trackable | |
86 | last_login_at = Column(DateTime()) | |
87 | current_login_at = Column(DateTime()) | |
88 | last_login_ip = Column(String(64)) | |
89 | current_login_ip = Column(String(64)) | |
90 | login_count = Column(Integer) | |
91 | ||
92 | # 2FA | |
93 | tf_primary_method = Column(String(64), nullable=True) | |
94 | tf_totp_secret = Column(String(255), nullable=True) | |
95 | tf_phone_number = Column(String(128), nullable=True) | |
96 | ||
97 | @declared_attr | |
98 | def roles(cls): | |
99 | return FsModels.db.relationship( | |
100 | "Role", | |
101 | secondary=FsModels.roles_users, | |
102 | backref=FsModels.db.backref("users", lazy="dynamic"), | |
103 | ) | |
104 | ||
105 | create_datetime = Column(DateTime, nullable=False, server_default=func.now()) | |
106 | update_datetime = Column( | |
107 | DateTime, | |
108 | nullable=False, | |
109 | server_default=func.now(), | |
110 | onupdate=datetime.datetime.utcnow, | |
111 | ) | |
112 | ||
113 | ||
114 | """ | |
115 | These are placeholders - not current used | |
116 | """ | |
117 | ||
118 | ||
119 | class FsOauth2ClientMixin(object): | |
120 | """ Oauth2 client """ | |
121 | ||
122 | id = Column(String(64), primary_key=True) | |
123 | ||
124 | @declared_attr | |
125 | def user_id(cls): | |
126 | return Column( | |
127 | Integer, ForeignKey("user.id", ondelete="CASCADE"), nullable=False | |
128 | ) | |
129 | ||
130 | @declared_attr | |
131 | def user(cls): | |
132 | return relationship("User") | |
133 | ||
134 | grant_type = Column(String(32), nullable=False) | |
135 | scopes = Column(UnicodeText(), default="") | |
136 | response_type = Column(UnicodeText, nullable=False, default="") | |
137 | redirect_uris = Column(UnicodeText()) | |
138 | ||
139 | ||
140 | class FsTokenMixin(object): | |
141 | """ (Bearer) Tokens that have been given out """ | |
142 | ||
143 | id = Column(Integer, primary_key=True) | |
144 | ||
145 | @declared_attr | |
146 | def client_id(cls): | |
147 | return Column( | |
148 | Integer, ForeignKey("oauth2_client.id", ondelete="CASCADE"), nullable=False | |
149 | ) | |
150 | ||
151 | # client = relationship("fs_oauth2_client") | |
152 | @declared_attr | |
153 | def user_id(cls): | |
154 | return Column( | |
155 | Integer, ForeignKey("user.id", ondelete="CASCADE"), nullable=False | |
156 | ) | |
157 | ||
158 | scopes = Column(UnicodeText(), default="") | |
159 | revoked = Column(Boolean(), nullable=False, default=False) | |
160 | access_token = Column(String(100), unique=True, nullable=False) | |
161 | refresh_token = Column(String(100), unique=True) | |
162 | issued_at = Column(DateTime, nullable=False, server_default=func.now()) | |
163 | expires_at = Column(DateTime()) |
0 | """ | |
1 | Copyright 2020 by J. Christopher Wagner (jwag). All rights reserved. | |
2 | :license: MIT, see LICENSE for more details. | |
3 | ||
4 | ||
5 | Complete models for all features when using Flask-SqlAlchemy | |
6 | ||
7 | BE AWARE: Once any version of this is shipped no changes can be made - instead | |
8 | a new version needs to be created. | |
9 | ||
10 | This is Version 2: | |
11 | - Add support for unified sign in. | |
12 | - Make username unique (but not required). | |
13 | """ | |
14 | ||
15 | from sqlalchemy import Column, String, Text | |
16 | from sqlalchemy.ext.declarative import declared_attr | |
17 | ||
18 | ||
19 | from .fsqla import FsModels as FsModelsV1 | |
20 | from .fsqla import FsUserMixin as FsUserMixinV1 | |
21 | from .fsqla import FsRoleMixin as FsRoleMixinV1 | |
22 | ||
23 | ||
24 | class FsModels(FsModelsV1): | |
25 | fs_model_version = 2 | |
26 | pass | |
27 | ||
28 | ||
29 | class FsRoleMixin(FsRoleMixinV1): | |
30 | pass | |
31 | ||
32 | ||
33 | class FsUserMixin(FsUserMixinV1): | |
34 | """ User information | |
35 | """ | |
36 | ||
37 | # Make username unique but not required. | |
38 | username = Column(String(255), unique=True, nullable=True) | |
39 | ||
40 | # unified sign in | |
41 | us_totp_secrets = Column(Text, nullable=True) | |
42 | us_phone_number = Column(String(128), nullable=True) | |
43 | ||
44 | # This is repeated since I couldn't figure out how to have it reference the | |
45 | # new version of FsModels. | |
46 | @declared_attr | |
47 | def roles(cls): | |
48 | return FsModels.db.relationship( | |
49 | "Role", | |
50 | secondary=FsModels.roles_users, | |
51 | backref=FsModels.db.backref("users", lazy="dynamic"), | |
52 | ) |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.passwordless | |
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Flask-Security passwordless module | |
6 | ||
7 | :copyright: (c) 2012 by Matt Wright. | |
8 | :license: MIT, see LICENSE for more details. | |
9 | """ | |
10 | ||
11 | from flask import current_app as app | |
12 | from werkzeug.local import LocalProxy | |
13 | ||
14 | from .signals import login_instructions_sent | |
15 | from .utils import config_value, get_token_status, url_for_security | |
16 | ||
17 | # Convenient references | |
18 | _security = LocalProxy(lambda: app.extensions["security"]) | |
19 | ||
20 | _datastore = LocalProxy(lambda: _security.datastore) | |
21 | ||
22 | ||
23 | def send_login_instructions(user): | |
24 | """Sends the login instructions email for the specified user. | |
25 | ||
26 | :param user: The user to send the instructions to | |
27 | :param token: The login token | |
28 | """ | |
29 | token = generate_login_token(user) | |
30 | login_link = url_for_security("token_login", token=token, _external=True) | |
31 | ||
32 | _security._send_mail( | |
33 | config_value("EMAIL_SUBJECT_PASSWORDLESS"), | |
34 | user.email, | |
35 | "login_instructions", | |
36 | user=user, | |
37 | login_link=login_link, | |
38 | ) | |
39 | ||
40 | login_instructions_sent.send( | |
41 | app._get_current_object(), user=user, login_token=token | |
42 | ) | |
43 | ||
44 | ||
45 | def generate_login_token(user): | |
46 | """Generates a unique login token for the specified user. | |
47 | ||
48 | :param user: The user the token belongs to | |
49 | """ | |
50 | return _security.login_serializer.dumps([str(user.id)]) | |
51 | ||
52 | ||
53 | def login_token_status(token): | |
54 | """Returns the expired status, invalid status, and user of a login token. | |
55 | For example:: | |
56 | ||
57 | expired, invalid, user = login_token_status('...') | |
58 | ||
59 | :param token: The login token | |
60 | """ | |
61 | return get_token_status(token, "login", "LOGIN") |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.phone_util | |
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Utility class for managing phone numbers | |
6 | ||
7 | :copyright: (c) 2020 by J. Christopher Wagner (jwag). | |
8 | :license: MIT, see LICENSE for more details. | |
9 | ||
10 | Avoid making 'phonenumbers' a required package unless needed. | |
11 | """ | |
12 | ||
13 | from .utils import config_value, get_message | |
14 | ||
15 | ||
16 | class PhoneUtil(object): | |
17 | """ | |
18 | Provide parsing and validation for user inputted phone numbers. | |
19 | Subclass this to use a different underlying phone number parsing library. | |
20 | ||
21 | To provide your own implementation, pass in the class as ``phone_util_cls`` | |
22 | at init time. Your class will be instantiated once prior to the first | |
23 | request being handled. | |
24 | ||
25 | .. versionadded:: 3.4.0 | |
26 | """ | |
27 | ||
28 | def validate_phone_number(self, input_data): | |
29 | """ Return ``None`` if a valid phone number else an error message. """ | |
30 | import phonenumbers | |
31 | ||
32 | try: | |
33 | z = phonenumbers.parse( | |
34 | input_data, region=config_value("PHONE_REGION_DEFAULT") | |
35 | ) | |
36 | if phonenumbers.is_valid_number(z): | |
37 | return None | |
38 | except phonenumbers.phonenumberutil.NumberParseException: | |
39 | pass | |
40 | return get_message("PHONE_INVALID")[0] | |
41 | ||
42 | def get_canonical_form(self, input_data): | |
43 | """ Validate and return a canonical form to be stored in DB | |
44 | and compared against. | |
45 | Returns ``None`` if input isn't a valid phone number. | |
46 | """ | |
47 | import phonenumbers | |
48 | ||
49 | try: | |
50 | z = phonenumbers.parse( | |
51 | input_data, region=config_value("PHONE_REGION_DEFAULT") | |
52 | ) | |
53 | if phonenumbers.is_valid_number(z): | |
54 | return phonenumbers.format_number( | |
55 | z, phonenumbers.PhoneNumberFormat.E164 | |
56 | ) | |
57 | return None | |
58 | except phonenumbers.phonenumberutil.NumberParseException: | |
59 | return None |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.quart_compat | |
3 | ~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Flask-Security quart compatibility modiles | |
6 | ||
7 | :copyright: (c) 2019 by Shinon. | |
8 | :license: MIT, see LICENSE for more details. | |
9 | ||
10 | This modules tests whether we are using quart or not | |
11 | we can test if the name of the imported flask is: quart.flask_patch | |
12 | """ | |
13 | import flask | |
14 | ||
15 | if "quart." in flask.__name__ or hasattr(flask, "_quart_patched"): # pragma: no cover | |
16 | is_quart = True | |
17 | else: | |
18 | is_quart = False | |
19 | ||
20 | ||
21 | @property | |
22 | def best(self): # pragma: no cover | |
23 | options = sorted( | |
24 | self.options, | |
25 | key=lambda option: (option.value != "*", option.quality, option.value), | |
26 | reverse=True, | |
27 | ) | |
28 | return options[0].value | |
29 | ||
30 | ||
31 | def get_quart_status(): | |
32 | """ | |
33 | Tests if we are using Quart Patched Flask or Vanilla Flask. | |
34 | :return: boolean value determining if it is quart patched flask or not | |
35 | """ | |
36 | return is_quart |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.recoverable | |
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Flask-Security recoverable module | |
6 | ||
7 | :copyright: (c) 2012 by Matt Wright. | |
8 | :license: MIT, see LICENSE for more details. | |
9 | """ | |
10 | ||
11 | from flask import current_app as app | |
12 | from werkzeug.local import LocalProxy | |
13 | ||
14 | from .signals import password_reset, reset_password_instructions_sent | |
15 | from .utils import ( | |
16 | config_value, | |
17 | get_token_status, | |
18 | hash_data, | |
19 | hash_password, | |
20 | url_for_security, | |
21 | verify_hash, | |
22 | ) | |
23 | ||
24 | # Convenient references | |
25 | _security = LocalProxy(lambda: app.extensions["security"]) | |
26 | ||
27 | _datastore = LocalProxy(lambda: _security.datastore) | |
28 | ||
29 | ||
30 | def send_reset_password_instructions(user): | |
31 | """Sends the reset password instructions email for the specified user. | |
32 | ||
33 | :param user: The user to send the instructions to | |
34 | """ | |
35 | token = generate_reset_password_token(user) | |
36 | reset_link = url_for_security("reset_password", token=token, _external=True) | |
37 | ||
38 | if config_value("SEND_PASSWORD_RESET_EMAIL"): | |
39 | _security._send_mail( | |
40 | config_value("EMAIL_SUBJECT_PASSWORD_RESET"), | |
41 | user.email, | |
42 | "reset_instructions", | |
43 | user=user, | |
44 | reset_link=reset_link, | |
45 | ) | |
46 | ||
47 | reset_password_instructions_sent.send( | |
48 | app._get_current_object(), user=user, token=token | |
49 | ) | |
50 | ||
51 | ||
52 | def send_password_reset_notice(user): | |
53 | """Sends the password reset notice email for the specified user. | |
54 | ||
55 | :param user: The user to send the notice to | |
56 | """ | |
57 | if config_value("SEND_PASSWORD_RESET_NOTICE_EMAIL"): | |
58 | _security._send_mail( | |
59 | config_value("EMAIL_SUBJECT_PASSWORD_NOTICE"), | |
60 | user.email, | |
61 | "reset_notice", | |
62 | user=user, | |
63 | ) | |
64 | ||
65 | ||
66 | def generate_reset_password_token(user): | |
67 | """Generates a unique reset password token for the specified user. | |
68 | ||
69 | :param user: The user to work with | |
70 | """ | |
71 | password_hash = hash_data(user.password) if user.password else None | |
72 | data = [str(user.id), password_hash] | |
73 | return _security.reset_serializer.dumps(data) | |
74 | ||
75 | ||
76 | def reset_password_token_status(token): | |
77 | """Returns the expired status, invalid status, and user of a password reset | |
78 | token. For example:: | |
79 | ||
80 | expired, invalid, user, data = reset_password_token_status('...') | |
81 | ||
82 | :param token: The password reset token | |
83 | """ | |
84 | expired, invalid, user, data = get_token_status( | |
85 | token, "reset", "RESET_PASSWORD", return_data=True | |
86 | ) | |
87 | if not invalid and user: | |
88 | if user.password: | |
89 | if not verify_hash(data[1], user.password): | |
90 | invalid = True | |
91 | ||
92 | return expired, invalid, user | |
93 | ||
94 | ||
95 | def update_password(user, password): | |
96 | """Update the specified user's password | |
97 | ||
98 | :param user: The user to update_password | |
99 | :param password: The unhashed new password | |
100 | """ | |
101 | user.password = hash_password(password) | |
102 | if config_value("BACKWARDS_COMPAT_AUTH_TOKEN_INVALID"): | |
103 | _datastore.set_uniquifier(user) | |
104 | _datastore.put(user) | |
105 | send_password_reset_notice(user) | |
106 | password_reset.send(app._get_current_object(), user=user) |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.registerable | |
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Flask-Security registerable module | |
6 | ||
7 | :copyright: (c) 2012 by Matt Wright. | |
8 | :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag). | |
9 | :license: MIT, see LICENSE for more details. | |
10 | """ | |
11 | ||
12 | import uuid | |
13 | ||
14 | from flask import current_app as app | |
15 | from werkzeug.local import LocalProxy | |
16 | ||
17 | from .confirmable import generate_confirmation_link | |
18 | from .signals import user_registered | |
19 | from .utils import config_value, do_flash, get_message, hash_password | |
20 | ||
21 | # Convenient references | |
22 | _security = LocalProxy(lambda: app.extensions["security"]) | |
23 | ||
24 | _datastore = LocalProxy(lambda: _security.datastore) | |
25 | ||
26 | ||
27 | def register_user(registration_form): | |
28 | """ | |
29 | Calls datastore to create user, triggers post-registration logic | |
30 | (e.g. sending confirmation link, sending registration mail) | |
31 | :param registration_form: form with user registration data | |
32 | :return: user instance | |
33 | """ | |
34 | ||
35 | user_model_kwargs = registration_form.to_dict(only_user=True) | |
36 | ||
37 | if not user_model_kwargs["password"]: | |
38 | # For no password - set an unguessable password. | |
39 | # Since we still allow 'plaintext' as a password scheme - can't use a simple | |
40 | # sentinel. | |
41 | user_model_kwargs["password"] = "NoPassword-" + uuid.uuid4().hex | |
42 | ||
43 | user_model_kwargs["password"] = hash_password(user_model_kwargs["password"]) | |
44 | user = _datastore.create_user(**user_model_kwargs) | |
45 | # This has always been here - but should probably be removed since in all other | |
46 | # cases we use a 'after_this_request(commit)'. Seems like this would break quart | |
47 | # compat as well? | |
48 | _datastore.commit() | |
49 | ||
50 | confirmation_link, token = None, None | |
51 | if _security.confirmable: | |
52 | confirmation_link, token = generate_confirmation_link(user) | |
53 | do_flash(*get_message("CONFIRM_REGISTRATION", email=user.email)) | |
54 | ||
55 | user_registered.send( | |
56 | app._get_current_object(), | |
57 | user=user, | |
58 | confirm_token=token, | |
59 | form_data=registration_form.to_dict(only_user=False), | |
60 | ) | |
61 | ||
62 | if config_value("SEND_REGISTER_EMAIL"): | |
63 | _security._send_mail( | |
64 | config_value("EMAIL_SUBJECT_REGISTER"), | |
65 | user.email, | |
66 | "welcome", | |
67 | user=user, | |
68 | confirmation_link=confirmation_link, | |
69 | ) | |
70 | ||
71 | return user |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.signals | |
3 | ~~~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Flask-Security signals module | |
6 | ||
7 | :copyright: (c) 2012 by Matt Wright. | |
8 | :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag). | |
9 | :license: MIT, see LICENSE for more details. | |
10 | """ | |
11 | ||
12 | import blinker | |
13 | ||
14 | signals = blinker.Namespace() | |
15 | ||
16 | user_authenticated = signals.signal("user-authenticated") | |
17 | ||
18 | user_registered = signals.signal("user-registered") | |
19 | ||
20 | user_confirmed = signals.signal("user-confirmed") | |
21 | ||
22 | confirm_instructions_sent = signals.signal("confirm-instructions-sent") | |
23 | ||
24 | login_instructions_sent = signals.signal("login-instructions-sent") | |
25 | ||
26 | password_reset = signals.signal("password-reset") | |
27 | ||
28 | password_changed = signals.signal("password-changed") | |
29 | ||
30 | reset_password_instructions_sent = signals.signal("password-reset-instructions-sent") | |
31 | ||
32 | tf_code_confirmed = signals.signal("tf-code-confirmed") | |
33 | ||
34 | tf_profile_changed = signals.signal("tf-profile-changed") | |
35 | ||
36 | tf_security_token_sent = signals.signal("tf-security-token-sent") | |
37 | ||
38 | tf_disabled = signals.signal("tf-disabled") | |
39 | ||
40 | us_security_token_sent = signals.signal("us-security-token-sent") | |
41 | ||
42 | us_profile_changed = signals.signal("us-profile-changed") |
0 | {% macro render_field_with_errors(field) %} | |
1 | <p> | |
2 | {{ field.label }} {{ field(**kwargs)|safe }} | |
3 | {% if field.errors %} | |
4 | <ul> | |
5 | {% for error in field.errors %} | |
6 | <li>{{ error }}</li> | |
7 | {% endfor %} | |
8 | </ul> | |
9 | {% endif %} | |
10 | </p> | |
11 | {% endmacro %} | |
12 | ||
13 | {% macro render_field(field) %} | |
14 | <p>{{ field(**kwargs)|safe }}</p> | |
15 | {% endmacro %} | |
16 | ||
17 | {% macro render_field_errors(field) %} | |
18 | <p> | |
19 | {% if field and field.errors %} | |
20 | <ul> | |
21 | {% for error in field.errors %} | |
22 | <li>{{ error }}</li> | |
23 | {% endfor %} | |
24 | </ul> | |
25 | {% endif %} | |
26 | </p> | |
27 | {% endmacro %} |
0 | {% if security.registerable or security.recoverable or security.confirmable or security.unified_signin %} | |
1 | <h2>{{ _('Menu') }}</h2> | |
2 | <ul> | |
3 | {% if not skip_login_menu %} | |
4 | <li><a href="{{ url_for_security('login') }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}">{{ _('Login') }}</a></li> | |
5 | {% endif %} | |
6 | {% if security.unified_signin and not skip_login_menu %} | |
7 | <li><a href="{{ url_for_security('us_signin') }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}">{{ _("Unified Sign In") }}</a><br/></li> | |
8 | {% endif %} | |
9 | {% if security.registerable %} | |
10 | <li><a href="{{ url_for_security('register') }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}">{{ _('Register') }}</a><br/></li> | |
11 | {% endif %} | |
12 | {% if security.recoverable %} | |
13 | <li><a href="{{ url_for_security('forgot_password') }}">{{ _('Forgot password') }}</a><br/></li> | |
14 | {% endif %} | |
15 | {% if security.confirmable %} | |
16 | <li><a href="{{ url_for_security('send_confirmation') }}">{{ _('Confirm account') }}</a></li> | |
17 | {% endif %} | |
18 | </ul> | |
19 | {% endif %} |
0 | {%- with messages = get_flashed_messages(with_categories=true) -%} | |
1 | {% if messages %} | |
2 | <ul class="flashes"> | |
3 | {% for category, message in messages %} | |
4 | <li class="{{ category }}">{{ message }}</li> | |
5 | {% endfor %} | |
6 | </ul> | |
7 | {% endif %} | |
8 | {%- endwith %} |
0 | {% block doc -%} | |
1 | <!DOCTYPE html> | |
2 | <html{% block html_attribs %}{% endblock html_attribs %}> | |
3 | {%- block html %} | |
4 | <head> | |
5 | {%- block head %} | |
6 | <title>{% block title %}{{ title|default }}{% endblock title %}</title> | |
7 | ||
8 | {%- block metas %} | |
9 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
10 | {%- endblock metas %} | |
11 | ||
12 | {%- block styles %} | |
13 | {%- endblock styles %} | |
14 | {%- endblock head %} | |
15 | </head> | |
16 | <body{% block body_attribs %}{% endblock body_attribs %}> | |
17 | {% block body -%} | |
18 | {% block navbar %} | |
19 | {%- endblock navbar %} | |
20 | {% block content -%} | |
21 | {%- endblock content %} | |
22 | ||
23 | {% block scripts %} | |
24 | {%- endblock scripts %} | |
25 | {%- endblock body %} | |
26 | </body> | |
27 | {%- endblock html %} | |
28 | </html> | |
29 | {% endblock doc -%} |
0 | {% extends "security/base.html" %} | |
1 | {% from "security/_macros.html" import render_field_with_errors, render_field %} | |
2 | ||
3 | {% block content %} | |
4 | {% include "security/_messages.html" %} | |
5 | <h1>{{ _('Change password') }}</h1> | |
6 | <form action="{{ url_for_security('change_password') }}" method="POST" name="change_password_form"> | |
7 | {{ change_password_form.hidden_tag() }} | |
8 | {{ render_field_with_errors(change_password_form.password) }} | |
9 | {{ render_field_with_errors(change_password_form.new_password) }} | |
10 | {{ render_field_with_errors(change_password_form.new_password_confirm) }} | |
11 | {{ render_field(change_password_form.submit) }} | |
12 | </form> | |
13 | {% endblock %} |
0 | <p>{{ _('Your password has been changed.') }}</p> | |
1 | {% if security.recoverable %} | |
2 | <p>{{ _('If you did not change your password,') }} <a href="{{ url_for_security('forgot_password', _external=True) }}">{{ _('click here to reset it') }}</a>.</p> | |
3 | {% endif %} |
0 | {{ _('Your password has been changed') }} | |
1 | {% if security.recoverable %} | |
2 | {{ _('If you did not change your password, click the link below to reset it.') }} | |
3 | {{ url_for_security('forgot_password', _external=True) }} | |
4 | {% endif %} |
+3
-0
0 | <p>{{ _('Please confirm your email through the link below:') }}</p> | |
1 | ||
2 | <p><a href="{{ confirmation_link }}">{{ _('Confirm my account') }}</a></p> |
+3
-0
0 | {{ _('Please confirm your email through the link below:') }} | |
1 | ||
2 | {{ confirmation_link }} |
+5
-0
0 | <p>{{ _('Welcome %(email)s!', email=user.email) }}</p> | |
1 | ||
2 | <p>{{ _('You can log into your account through the link below:') }}</p> | |
3 | ||
4 | <p><a href="{{ login_link }}">{{ _('Login now') }}</a></p> |
+5
-0
0 | {{ _('Welcome %(email)s!', email=user.email) }} | |
1 | ||
2 | {{ _('You can log into your account through the link below:') }} | |
3 | ||
4 | {{ login_link }} |
+1
-0
0 | <p><a href="{{ reset_link }}">{{ _('Click here to reset your password') }}</a></p> |
+3
-0
0 | {{ _('Click the link below to reset your password:') }} | |
1 | ||
2 | {{ reset_link }} |
0 | <p>{{ _('Your password has been reset') }}</p> |
0 | {{ _('Your password has been reset') }} |
+3
-0
0 | <p>{{ _("Welcome") }} {{ username }}!</p> | |
1 | ||
2 | <p>{{ _("You can log into your account using the following code:") }} {{ token }}</p> |
+3
-0
0 | {{ _("Welcome") }} {{ username }}! | |
1 | ||
2 | {{ _("You can log into your account using the following code:") }} {{ token }} |
+1
-0
0 | <p> {{ user.email }} {{ _("can not access mail account") }}</p> |
0 | {{ user.email }} {{ _("can not access mail account") }} |
0 | <p>{{ _("Welcome") }} {{ username }}!</p> | |
1 | ||
2 | <p>{{ _("You can sign into your account using the following code:") }} {{ token }}</p> | |
3 | ||
4 | {% if login_link %} | |
5 | <p>{{ _("Or use the the link below:") }}</p> | |
6 | ||
7 | <p><a href="{{ login_link }}">{{ _("Sign In") }}</a></p> | |
8 | {% endif %} |
0 | {{ _("Welcome") }} {{ username }}! | |
1 | ||
2 | {{ _("You can sign into your account using the following code:") }} {{ token }} | |
3 | ||
4 | {% if login_link %} | |
5 | ||
6 | {{ _("Or use the link below:") }} | |
7 | ||
8 | {{ login_link }} | |
9 | {% endif %} |
0 | <p>{{ _('Welcome %(email)s!', email=user.email) }}</p> | |
1 | ||
2 | {% if security.confirmable %} | |
3 | <p>{{ _('You can confirm your email through the link below:') }}</p> | |
4 | ||
5 | <p><a href="{{ confirmation_link }}">{{ _('Confirm my account') }}</a></p> | |
6 | {% endif %} |
0 | {{ _('Welcome %(email)s!', email=user.email) }} | |
1 | ||
2 | {% if security.confirmable %} | |
3 | {{ _('You can confirm your email through the link below:') }} | |
4 | ||
5 | {{ confirmation_link }} | |
6 | {% endif %} |
0 | {% extends "security/base.html" %} | |
1 | {% from "security/_macros.html" import render_field_with_errors, render_field %} | |
2 | ||
3 | {% block content %} | |
4 | {% include "security/_messages.html" %} | |
5 | <h1>{{ _('Send password reset instructions') }}</h1> | |
6 | <form action="{{ url_for_security('forgot_password') }}" method="POST" name="forgot_password_form"> | |
7 | {{ forgot_password_form.hidden_tag() }} | |
8 | {{ render_field_with_errors(forgot_password_form.email) }} | |
9 | {{ render_field(forgot_password_form.submit) }} | |
10 | </form> | |
11 | {% include "security/_menu.html" %} | |
12 | {% endblock %} |
0 | {% extends "security/base.html" %} | |
1 | {% from "security/_macros.html" import render_field_with_errors, render_field, render_field_errors %} | |
2 | ||
3 | {% block content %} | |
4 | {% include "security/_messages.html" %} | |
5 | <h1>{{ _('Login') }}</h1> | |
6 | <form action="{{ url_for_security('login') }}" method="POST" name="login_user_form"> | |
7 | {{ login_user_form.hidden_tag() }} | |
8 | {{ render_field_with_errors(login_user_form.email) }} | |
9 | {{ render_field_with_errors(login_user_form.password) }} | |
10 | {{ render_field_with_errors(login_user_form.remember) }} | |
11 | {{ render_field_errors(login_user_form.csrf_token) }} | |
12 | {{ render_field(login_user_form.submit) }} | |
13 | </form> | |
14 | {% include "security/_menu.html" %} | |
15 | {% endblock %} |
0 | {% extends "security/base.html" %} | |
1 | {% from "security/_macros.html" import render_field_with_errors, render_field %} | |
2 | ||
3 | {% block content %} | |
4 | {% include "security/_messages.html" %} | |
5 | <h1>{{ _('Register') }}</h1> | |
6 | <form action="{{ url_for_security('register') }}" method="POST" name="register_user_form"> | |
7 | {{ register_user_form.hidden_tag() }} | |
8 | {{ render_field_with_errors(register_user_form.email) }} | |
9 | {{ render_field_with_errors(register_user_form.password) }} | |
10 | {% if register_user_form.password_confirm %} | |
11 | {{ render_field_with_errors(register_user_form.password_confirm) }} | |
12 | {% endif %} | |
13 | {{ render_field(register_user_form.submit) }} | |
14 | </form> | |
15 | {% include "security/_menu.html" %} | |
16 | {% endblock %} |
0 | {% extends "security/base.html" %} | |
1 | {% from "security/_macros.html" import render_field_with_errors, render_field %} | |
2 | ||
3 | {% block content %} | |
4 | {% include "security/_messages.html" %} | |
5 | <h1>{{ _('Reset password') }}</h1> | |
6 | <form action="{{ url_for_security('reset_password', token=reset_password_token) }}" method="POST" name="reset_password_form"> | |
7 | {{ reset_password_form.hidden_tag() }} | |
8 | {{ render_field_with_errors(reset_password_form.password) }} | |
9 | {{ render_field_with_errors(reset_password_form.password_confirm) }} | |
10 | {{ render_field(reset_password_form.submit) }} | |
11 | </form> | |
12 | {% include "security/_menu.html" %} | |
13 | {% endblock %} |
0 | {% extends "security/base.html" %} | |
1 | {% from "security/_macros.html" import render_field_with_errors, render_field %} | |
2 | ||
3 | {% block content %} | |
4 | {% include "security/_messages.html" %} | |
5 | <h1>{{ _('Resend confirmation instructions') }}</h1> | |
6 | <form action="{{ url_for_security('send_confirmation') }}" method="POST" name="send_confirmation_form"> | |
7 | {{ send_confirmation_form.hidden_tag() }} | |
8 | {{ render_field_with_errors(send_confirmation_form.email) }} | |
9 | {{ render_field(send_confirmation_form.submit) }} | |
10 | </form> | |
11 | {% include "security/_menu.html" %} | |
12 | {% endblock %} |
0 | {% extends "security/base.html" %} | |
1 | {% from "security/_macros.html" import render_field_with_errors, render_field %} | |
2 | ||
3 | {% block content %} | |
4 | {% include "security/_messages.html" %} | |
5 | <h1>{{ _('Login') }}</h1> | |
6 | <form action="{{ url_for_security('login') }}" method="POST" name="send_login_form"> | |
7 | {{ send_login_form.hidden_tag() }} | |
8 | {{ render_field_with_errors(send_login_form.email) }} | |
9 | {{ render_field(send_login_form.submit) }} | |
10 | </form> | |
11 | {% include "security/_menu.html" %} | |
12 | {% endblock %} |
0 | {% extends "security/base.html" %} | |
1 | {% from "security/_macros.html" import render_field_with_errors, render_field, render_field_no_label, render_field_errors %} | |
2 | ||
3 | {% block content %} | |
4 | {% include "security/_messages.html" %} | |
5 | <h1>{{ _("Two-factor authentication adds an extra layer of security to your account") }}</h1> | |
6 | <h2>{{ _("In addition to your username and password, you'll need to use a code that we will send you") }}</h2> | |
7 | <form action="{{ url_for_security("two_factor_setup") }}" method="POST" name="two_factor_setup_form"> | |
8 | {{ two_factor_setup_form.hidden_tag() }} | |
9 | {% for subfield in two_factor_setup_form.setup %} | |
10 | {% if subfield.data in choices %} | |
11 | {{ render_field_with_errors(subfield) }} | |
12 | {% endif %} | |
13 | {% endfor %} | |
14 | {{ render_field_errors(two_factor_setup_form.setup) }} | |
15 | {{ render_field(two_factor_setup_form.submit) }} | |
16 | {% if chosen_method=="email" and chosen_method in choices %} | |
17 | <p>{{ _("To complete logging in, please enter the code sent to your mail") }}</p> | |
18 | {% endif %} | |
19 | {% if chosen_method=="authenticator" and chosen_method in choices %} | |
20 | <p>{{ _("Open your authenticator app on your device and scan the following qrcode to start receiving codes:") }}</p> | |
21 | <p><img alt="{{ _("Two factor authentication code") }}" id="qrcode" src="{{ url_for_security("two_factor_qrcode") }}"></p> | |
22 | {% endif %} | |
23 | {% if chosen_method=="sms" and chosen_method in choices %} | |
24 | <p>{{ _("To Which Phone Number Should We Send Code To?") }}</p> | |
25 | {{ two_factor_setup_form.hidden_tag() }} | |
26 | {{ render_field_with_errors(two_factor_setup_form.phone, placeholder="enter phone number") }} | |
27 | {{ render_field(two_factor_setup_form.submit) }} | |
28 | {% endif %} | |
29 | </form> | |
30 | <form action="{{ url_for_security("two_factor_token_validation") }}" method="POST" | |
31 | name="two_factor_verify_code_form"> | |
32 | {{ two_factor_verify_code_form.hidden_tag() }} | |
33 | {{ render_field_with_errors(two_factor_verify_code_form.code) }} | |
34 | {{ render_field(two_factor_verify_code_form.submit) }} | |
35 | </form> | |
36 | {% include "security/_menu.html" %} | |
37 | {% endblock %} |
+26
-0
0 | {% extends "security/base.html" %} | |
1 | {% from "security/_macros.html" import render_field_with_errors, render_field %} | |
2 | ||
3 | {% block content %} | |
4 | {% include "security/_messages.html" %} | |
5 | <h1>{{ _("Two-factor Authentication") }}</h1> | |
6 | <h2>{{ _("Please enter your authentication code") }}</h2> | |
7 | <form action="{{ url_for_security("two_factor_token_validation") }}" method="POST" | |
8 | name="two_factor_verify_code_form"> | |
9 | {{ two_factor_verify_code_form.hidden_tag() }} | |
10 | {{ render_field_with_errors(two_factor_verify_code_form.code, placeholder="enter code") }} | |
11 | {{ render_field(two_factor_verify_code_form.submit) }} | |
12 | </form> | |
13 | <form action="{{ url_for_security("two_factor_rescue") }}" method="POST" name="two_factor_rescue_form"> | |
14 | {{ two_factor_rescue_form.hidden_tag() }} | |
15 | {{ render_field_with_errors(two_factor_rescue_form.help_setup) }} | |
16 | {% if problem=="lost_device" %} | |
17 | <p>{{ _("The code for authentication was sent to your email address") }}</p> | |
18 | {% endif %} | |
19 | {% if problem=="no_mail_access" %} | |
20 | <p>{{ _("A mail was sent to us in order to reset your application account") }}</p> | |
21 | {% endif %} | |
22 | {{ render_field(two_factor_rescue_form.submit) }} | |
23 | </form> | |
24 | {% include "security/_menu.html" %} | |
25 | {% endblock %} |
+13
-0
0 | {% extends "security/base.html" %} | |
1 | {% from "security/_macros.html" import render_field_with_errors, render_field %} | |
2 | ||
3 | {% block content %} | |
4 | {% include "security/_messages.html" %} | |
5 | <h1>{{ _("Please Enter Your Password") }}</h1> | |
6 | <form action="{{ url_for_security("two_factor_verify_password") }}" method="POST" | |
7 | name="two_factor_verify_password_form"> | |
8 | {{ two_factor_verify_password_form.hidden_tag() }} | |
9 | {{ render_field_with_errors(two_factor_verify_password_form.password, placeholder="enter password") }} | |
10 | {{ render_field(two_factor_verify_password_form.submit) }} | |
11 | </form> | |
12 | {% endblock %} |
0 | {% extends "security/base.html" %} | |
1 | {% from "security/_macros.html" import render_field_with_errors, render_field, render_field_errors %} | |
2 | ||
3 | {% block content %} | |
4 | {% include "security/_messages.html" %} | |
5 | <h1>{{ _("Setup Unified Sign In options") }}</h1> | |
6 | <form action="{{ url_for_security("us_setup") }}" method="POST" | |
7 | name="us_setup_form"> | |
8 | {{ us_setup_form.hidden_tag() }} | |
9 | {% if setup_methods %} | |
10 | <p>Currently Active options: | |
11 | {% if active_methods %} | |
12 | {{ ", ".join(active_methods) }} | |
13 | {% else %} | |
14 | None. | |
15 | {% endif %} | |
16 | </p> | |
17 | {% for subfield in us_setup_form.chosen_method %} | |
18 | {% if subfield.data in available_methods %} | |
19 | {{ render_field_with_errors(subfield) }} | |
20 | {% endif %} | |
21 | {% endfor %} | |
22 | {{ render_field_errors(us_setup_form.chosen_method) }} | |
23 | {% if "sms" in available_methods %} | |
24 | {{ render_field_with_errors(us_setup_form.phone) }} | |
25 | {% endif %} | |
26 | {% if chosen_method == "authenticator" %} | |
27 | <p>{{ _("Open your authenticator app on your device and scan the following qrcode to start receiving codes:") }}</p> | |
28 | <p><img alt="{{ _("Passwordless QRCode") }}" id="qrcode" src="{{ url_for_security("us_qrcode", token=state) }}"></p> | |
29 | {% endif %} | |
30 | {% if code_sent %} | |
31 | <p>{{ _("Code has been sent") }} | |
32 | {% endif %} | |
33 | {{ render_field(us_setup_form.submit) }} | |
34 | {% else %} | |
35 | <h3>{{ _("No methods have been enabled - nothing to setup") }}</h3> | |
36 | {% endif %} | |
37 | </form> | |
38 | {% if state %} | |
39 | <form action="{{ url_for_security("us_setup_validate", token=state) }}" method="POST" | |
40 | name="us_setup_validate_form"> | |
41 | {{ us_setup_validate_form.hidden_tag() }} | |
42 | {{ render_field_with_errors(us_setup_validate_form.passcode) }} | |
43 | {{ render_field(us_setup_validate_form.submit) }} | |
44 | </form> | |
45 | {% endif %} | |
46 | {% endblock %} |
0 | {% extends "security/base.html" %} | |
1 | {% from "security/_macros.html" import render_field_with_errors, render_field, render_field_errors %} | |
2 | ||
3 | {% block content %} | |
4 | {% include "security/_messages.html" %} | |
5 | <h1>{{ _("Sign In") }}</h1> | |
6 | <form action="{{ url_for_security("us_signin") }}" method="POST" | |
7 | name="us_signin_form"> | |
8 | {{ us_signin_form.hidden_tag() }} | |
9 | {{ render_field_with_errors(us_signin_form.identity) }} | |
10 | {{ render_field_with_errors(us_signin_form.passcode) }} | |
11 | {{ render_field_with_errors(us_signin_form.remember) }} | |
12 | {{ render_field(us_signin_form.submit) }} | |
13 | {% if code_methods %} | |
14 | <h4>{{ _("Request one-time code be sent") }}</h4> | |
15 | {% for subfield in us_signin_form.chosen_method %} | |
16 | {% if subfield.data in code_methods %} | |
17 | {{ render_field_with_errors(subfield) }} | |
18 | {% endif %} | |
19 | {% endfor %} | |
20 | {{ render_field_errors(us_signin_form.chosen_method) }} | |
21 | {% if code_sent %} | |
22 | <p>{{ _("Code has been sent") }} | |
23 | {% endif %} | |
24 | {{ render_field(us_signin_form.submit_send_code, formaction=url_for_security('us_signin_send_code')) }} | |
25 | {% endif %} | |
26 | </form> | |
27 | {% include "security/_menu.html" %} | |
28 | {% endblock %} |
0 | {% extends "security/base.html" %} | |
1 | {% from "security/_macros.html" import render_field_with_errors, render_field, render_field_errors %} | |
2 | ||
3 | {% block content %} | |
4 | {% include "security/_messages.html" %} | |
5 | <h1>{{ _("Please re-authenticate") }}</h1> | |
6 | <form action="{{ url_for_security("us_verify") }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}" method="POST" | |
7 | name="us_verify_form"> | |
8 | {{ us_verify_form.hidden_tag() }} | |
9 | {{ render_field_with_errors(us_verify_form.passcode) }} | |
10 | {{ render_field(us_verify_form.submit) }} | |
11 | {% if code_methods %} | |
12 | <h4>{{ _("Request one-time code be sent") }}</h4> | |
13 | {% for subfield in us_verify_form.chosen_method %} | |
14 | {% if subfield.data in code_methods %} | |
15 | {{ render_field_with_errors(subfield) }} | |
16 | {% endif %} | |
17 | {% endfor %} | |
18 | {{ render_field_errors(us_verify_form.chosen_method) }} | |
19 | {% if code_sent %} | |
20 | <p>{{ _("Code has been sent") }} | |
21 | {% endif %} | |
22 | {{ render_field(us_verify_form.submit_send_code, formaction=send_code_to) }} | |
23 | {% endif %} | |
24 | </form> | |
25 | {% include "security/_menu.html" %} | |
26 | {% endblock %} |
0 | {% extends "security/base.html" %} | |
1 | {% from "security/_macros.html" import render_field_with_errors, render_field %} | |
2 | ||
3 | {% block content %} | |
4 | {% include "security/_messages.html" %} | |
5 | <h1>{{ _("Please Enter Your Password") }}</h1> | |
6 | <form action="{{ url_for_security("verify") }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}" method="POST" | |
7 | name="verify_form"> | |
8 | {{ verify_form.hidden_tag() }} | |
9 | {{ render_field_with_errors(verify_form.password) }} | |
10 | {{ render_field(verify_form.submit) }} | |
11 | </form> | |
12 | {% endblock %} |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.totp | |
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Flask-Security TOTP (Timed-One-Time-Passwords) module | |
6 | ||
7 | :copyright: (c) 2019 by J. Christopher Wagner (jwag). | |
8 | :license: MIT, see LICENSE for more details. | |
9 | """ | |
10 | ||
11 | from passlib.totp import TOTP, TokenError | |
12 | ||
13 | ||
14 | class Totp(object): | |
15 | """ Encapsulate usage of Passlib TOTP functionality. | |
16 | ||
17 | Flask-Security doesn't implement any replay-attack protection out of the box | |
18 | as suggested by: | |
19 | https://passlib.readthedocs.io/en/stable/narr/totp-tutorial.html#match-verify | |
20 | ||
21 | Subclass this and implement the get/set last_counter methods. Your subclass can | |
22 | be registered at Flask-Security creation/initialization time. | |
23 | ||
24 | .. versionadded:: 3.4.0 | |
25 | ||
26 | """ | |
27 | ||
28 | def __init__(self, secrets, issuer): | |
29 | """ Initialize a totp factory. | |
30 | secrets are used to encrypt the per-user totp_secret on disk. | |
31 | """ | |
32 | # This should be a dict with at least one entry | |
33 | if not isinstance(secrets, dict) or len(secrets) < 1: | |
34 | raise ValueError("secrets needs to be a dict with at least one entry") | |
35 | self._totp = TOTP.using(issuer=issuer, secrets=secrets) | |
36 | ||
37 | def generate_totp_password(self, totp_secret): | |
38 | """Get time-based one-time password on the basis of given secret and time | |
39 | :param totp_secret: the unique shared secret of the user | |
40 | """ | |
41 | return self._totp.from_source(totp_secret).generate().token | |
42 | ||
43 | def generate_totp_secret(self): | |
44 | """ Create new user-unique totp_secret. | |
45 | ||
46 | We return an encrypted json string so that when sent in a cookie or | |
47 | sent to DB - it is encrypted. | |
48 | ||
49 | """ | |
50 | return self._totp.new().to_json(encrypt=True) | |
51 | ||
52 | def verify_totp(self, token, totp_secret, user, window=0): | |
53 | """ Verifies token for specific user. | |
54 | ||
55 | :param token: token to be check against user's secret | |
56 | :param totp_secret: the unique shared secret of the user | |
57 | :param user: User model | |
58 | :param window: optional. How far backward and forward in time to search | |
59 | for a match. Measured in seconds. | |
60 | :return: True if match | |
61 | """ | |
62 | ||
63 | # TODO - in old implementation using onetimepass window was described | |
64 | # as 'compensate for clock skew) and 'interval_length' would say how long | |
65 | # the token is good for. | |
66 | # In passlib - 'window' means how far back and forward to look and 'clock_skew' | |
67 | # is specifically for well, clock slew. | |
68 | try: | |
69 | tmatch = self._totp.verify( | |
70 | token, | |
71 | totp_secret, | |
72 | window=window, | |
73 | last_counter=self.get_last_counter(user), | |
74 | ) | |
75 | self.set_last_counter(user, tmatch) | |
76 | return True | |
77 | ||
78 | except TokenError: | |
79 | return False | |
80 | ||
81 | def get_totp_uri(self, username, totp_secret): | |
82 | """ Generate provisioning url for use with the qrcode | |
83 | scanner built into the app | |
84 | ||
85 | :param username: username/email of the current user | |
86 | :param totp_secret: a unique shared secret of the user | |
87 | """ | |
88 | tp = self._totp.from_source(totp_secret) | |
89 | return tp.to_uri(username) | |
90 | ||
91 | def get_last_counter(self, user): | |
92 | """ Implement this to fetch stored last_counter from cache. | |
93 | ||
94 | :param user: User model | |
95 | :return: last_counter as stored in set_last_counter() | |
96 | """ | |
97 | return None | |
98 | ||
99 | def set_last_counter(self, user, tmatch): | |
100 | """ Implement this to cache last_counter. | |
101 | ||
102 | :param user: User model | |
103 | :param tmatch: a TotpMatch as returned from totp.verify() | |
104 | """ | |
105 | pass |
+628
-0
0 | # Catalan (Spain) translations for Flask-Security. | |
1 | # Copyright (C) 2017 DINSIC | |
2 | # This file is distributed under the same license as the Flask-Security | |
3 | # project. | |
4 | # Orestes Sanchez <[email protected]>, 2018. | |
5 | # | |
6 | msgid "" | |
7 | msgstr "" | |
8 | "Project-Id-Version: Flask-Security 3.1.0\n" | |
9 | "Report-Msgid-Bugs-To: [email protected]\n" | |
10 | "POT-Creation-Date: 2020-04-19 13:18-0700\n" | |
11 | "PO-Revision-Date: 2019-06-16 00:12+0200\n" | |
12 | "Last-Translator: Orestes Sanchez <[email protected]>\n" | |
13 | "Language: ca_ES\n" | |
14 | "Language-Team: \n" | |
15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" | |
16 | "MIME-Version: 1.0\n" | |
17 | "Content-Type: text/plain; charset=utf-8\n" | |
18 | "Content-Transfer-Encoding: 8bit\n" | |
19 | "Generated-By: Babel 2.8.0\n" | |
20 | ||
21 | #: flask_security/core.py:207 | |
22 | msgid "Login Required" | |
23 | msgstr "Per poder veure la pàgina sol·licitada és necessari iniciar la sessió" | |
24 | ||
25 | #: flask_security/core.py:208 | |
26 | #: flask_security/templates/security/email/two_factor_instructions.html:1 | |
27 | #: flask_security/templates/security/email/us_instructions.html:1 | |
28 | msgid "Welcome" | |
29 | msgstr "Benvingut" | |
30 | ||
31 | #: flask_security/core.py:209 | |
32 | msgid "Please confirm your email" | |
33 | msgstr "Si us plau, confirmeu el vostre correu electrònic" | |
34 | ||
35 | #: flask_security/core.py:210 | |
36 | msgid "Login instructions" | |
37 | msgstr "Instruccions d'inici de la sessió" | |
38 | ||
39 | #: flask_security/core.py:211 | |
40 | #: flask_security/templates/security/email/reset_notice.html:1 | |
41 | msgid "Your password has been reset" | |
42 | msgstr "S'ha restablit la teva contrasenya" | |
43 | ||
44 | #: flask_security/core.py:212 | |
45 | msgid "Your password has been changed" | |
46 | msgstr "S'ha canviat la teva contrasenya" | |
47 | ||
48 | #: flask_security/core.py:213 | |
49 | msgid "Password reset instructions" | |
50 | msgstr "Instruccions de recuperació de la contrasenya" | |
51 | ||
52 | #: flask_security/core.py:216 | |
53 | msgid "Two-factor Login" | |
54 | msgstr "" | |
55 | ||
56 | #: flask_security/core.py:217 | |
57 | msgid "Two-factor Rescue" | |
58 | msgstr "" | |
59 | ||
60 | #: flask_security/core.py:266 | |
61 | msgid "Verification Code" | |
62 | msgstr "" | |
63 | ||
64 | #: flask_security/core.py:282 | |
65 | msgid "Input not appropriate for requested API" | |
66 | msgstr "" | |
67 | ||
68 | #: flask_security/core.py:283 | |
69 | msgid "You do not have permission to view this resource." | |
70 | msgstr "No tens permís d'accés per a consultar aquest recurs." | |
71 | ||
72 | #: flask_security/core.py:285 | |
73 | msgid "You are not authenticated. Please supply the correct credentials." | |
74 | msgstr "" | |
75 | ||
76 | #: flask_security/core.py:289 | |
77 | #, fuzzy | |
78 | msgid "You must re-authenticate to access this endpoint" | |
79 | msgstr "Has d'iniciar una nova sessió per tal d'accedir a aquesta pàgina." | |
80 | ||
81 | #: flask_security/core.py:293 | |
82 | #, python-format | |
83 | msgid "Thank you. Confirmation instructions have been sent to %(email)s." | |
84 | msgstr "" | |
85 | "Moltes gràcies. S'ha enviat un correu electrònic a %(email)s amb " | |
86 | "instruccions per confirmar el teu compte." | |
87 | ||
88 | #: flask_security/core.py:296 | |
89 | msgid "Thank you. Your email has been confirmed." | |
90 | msgstr "Moltes gràcies. S'ha confirmat el teu correu electrònic." | |
91 | ||
92 | #: flask_security/core.py:297 | |
93 | msgid "Your email has already been confirmed." | |
94 | msgstr "El teu correu electrònic ja s'havia confirmat." | |
95 | ||
96 | #: flask_security/core.py:298 | |
97 | msgid "Invalid confirmation token." | |
98 | msgstr "Token de confirmació no vàlid." | |
99 | ||
100 | #: flask_security/core.py:300 | |
101 | #, python-format | |
102 | msgid "%(email)s is already associated with an account." | |
103 | msgstr "%(email)s ja es associat amb un compte." | |
104 | ||
105 | #: flask_security/core.py:303 | |
106 | msgid "Password does not match" | |
107 | msgstr "La contrasenya no coincideix" | |
108 | ||
109 | #: flask_security/core.py:304 | |
110 | msgid "Passwords do not match" | |
111 | msgstr "Les contrasenyes no coincideixen" | |
112 | ||
113 | #: flask_security/core.py:305 | |
114 | msgid "Redirections outside the domain are forbidden" | |
115 | msgstr "Les redireccions a llocs web externes s'han prohibit" | |
116 | ||
117 | #: flask_security/core.py:307 | |
118 | #, python-format | |
119 | msgid "Instructions to reset your password have been sent to %(email)s." | |
120 | msgstr "" | |
121 | "Les instruccions per restablir la teva contrasenya s'han enviat a " | |
122 | "%(email)s." | |
123 | ||
124 | #: flask_security/core.py:311 | |
125 | #, python-format | |
126 | msgid "" | |
127 | "You did not reset your password within %(within)s. New instructions have " | |
128 | "been sent to %(email)s." | |
129 | msgstr "" | |
130 | "No vas restablir la teva contrasenya abans de %(within)s. S'han enviat " | |
131 | "noves instruccions a %(email)s." | |
132 | ||
133 | #: flask_security/core.py:317 | |
134 | msgid "Invalid reset password token." | |
135 | msgstr "El token per restablir la contrasenya no és vàlid." | |
136 | ||
137 | #: flask_security/core.py:318 | |
138 | msgid "Email requires confirmation." | |
139 | msgstr "El correu electrònic requereix d'una confirmació." | |
140 | ||
141 | #: flask_security/core.py:320 | |
142 | #, python-format | |
143 | msgid "Confirmation instructions have been sent to %(email)s." | |
144 | msgstr "Les instruccions de confirmació s'han enviat a %(email)s." | |
145 | ||
146 | #: flask_security/core.py:324 | |
147 | #, python-format | |
148 | msgid "" | |
149 | "You did not confirm your email within %(within)s. New instructions to " | |
150 | "confirm your email have been sent to %(email)s." | |
151 | msgstr "" | |
152 | "No vas confirmar el teu correu electrònic abans de %(within)s. S'han " | |
153 | "enviat noves instruccions a %(email)s." | |
154 | ||
155 | #: flask_security/core.py:332 | |
156 | #, python-format | |
157 | msgid "" | |
158 | "You did not login within %(within)s. New instructions to login have been " | |
159 | "sent to %(email)s." | |
160 | msgstr "" | |
161 | "No vas iniciar la sessió abans de %(within)s. S'han enviat noves " | |
162 | "instruccions a %(email)s." | |
163 | ||
164 | #: flask_security/core.py:339 | |
165 | #, python-format | |
166 | msgid "Instructions to login have been sent to %(email)s." | |
167 | msgstr "S'han enviat instruccions per l'inici de sessió a %(email)s." | |
168 | ||
169 | #: flask_security/core.py:342 | |
170 | msgid "Invalid login token." | |
171 | msgstr "Token de d'inici de sessió no vàlid." | |
172 | ||
173 | #: flask_security/core.py:343 | |
174 | msgid "Account is disabled." | |
175 | msgstr "el compte està desactivat." | |
176 | ||
177 | #: flask_security/core.py:344 | |
178 | msgid "Email not provided" | |
179 | msgstr "No s'ha inclòs el correu electrònic" | |
180 | ||
181 | #: flask_security/core.py:345 | |
182 | msgid "Invalid email address" | |
183 | msgstr "Adreça de correu electrònic no vàlida" | |
184 | ||
185 | #: flask_security/core.py:346 | |
186 | #, fuzzy | |
187 | msgid "Invalid code" | |
188 | msgstr "Contrasenya no vàlida" | |
189 | ||
190 | #: flask_security/core.py:347 | |
191 | msgid "Password not provided" | |
192 | msgstr "No s'ha inclòs la contrasenya" | |
193 | ||
194 | #: flask_security/core.py:348 | |
195 | msgid "No password is set for this user" | |
196 | msgstr "No hi ha cap contrasenya per a l'usuari" | |
197 | ||
198 | #: flask_security/core.py:350 | |
199 | #, fuzzy, python-format | |
200 | msgid "Password must be at least %(length)s characters" | |
201 | msgstr "La contrasenya ha de tenir al menys 6 caràcters" | |
202 | ||
203 | #: flask_security/core.py:353 | |
204 | msgid "Password not complex enough" | |
205 | msgstr "" | |
206 | ||
207 | #: flask_security/core.py:354 | |
208 | msgid "Password on breached list" | |
209 | msgstr "" | |
210 | ||
211 | #: flask_security/core.py:356 | |
212 | msgid "Failed to contact breached passwords site" | |
213 | msgstr "" | |
214 | ||
215 | #: flask_security/core.py:359 | |
216 | msgid "Phone number not valid e.g. missing country code" | |
217 | msgstr "" | |
218 | ||
219 | #: flask_security/core.py:360 | |
220 | msgid "Specified user does not exist" | |
221 | msgstr "L'usuari no existeix" | |
222 | ||
223 | #: flask_security/core.py:361 | |
224 | msgid "Invalid password" | |
225 | msgstr "Contrasenya no vàlida" | |
226 | ||
227 | #: flask_security/core.py:362 | |
228 | msgid "Password or code submitted is not valid" | |
229 | msgstr "" | |
230 | ||
231 | #: flask_security/core.py:363 | |
232 | msgid "You have successfully logged in." | |
233 | msgstr "La sessió s'ha iniciat amb èxit." | |
234 | ||
235 | #: flask_security/core.py:364 | |
236 | msgid "Forgot password?" | |
237 | msgstr "Has oblidat la teva contrasenya?" | |
238 | ||
239 | #: flask_security/core.py:366 | |
240 | msgid "" | |
241 | "You successfully reset your password and you have been logged in " | |
242 | "automatically." | |
243 | msgstr "" | |
244 | "Has restablert la teva contrasenya amb èxit i s'ha iniciat la sessió " | |
245 | "automàticament." | |
246 | ||
247 | #: flask_security/core.py:373 | |
248 | msgid "Your new password must be different than your previous password." | |
249 | msgstr "La nova contrasenya ha de ser diferent de l'anterior." | |
250 | ||
251 | #: flask_security/core.py:376 | |
252 | msgid "You successfully changed your password." | |
253 | msgstr "La teva contrasenya s'ha modificat amb èxit." | |
254 | ||
255 | #: flask_security/core.py:377 | |
256 | msgid "Please log in to access this page." | |
257 | msgstr "Has d'iniciar sessió per tal d'accedir a aquesta pàgina." | |
258 | ||
259 | #: flask_security/core.py:378 | |
260 | msgid "Please reauthenticate to access this page." | |
261 | msgstr "Has d'iniciar una nova sessió per tal d'accedir a aquesta pàgina." | |
262 | ||
263 | #: flask_security/core.py:379 | |
264 | msgid "Reauthentication successful" | |
265 | msgstr "" | |
266 | ||
267 | #: flask_security/core.py:381 | |
268 | msgid "You can only access this endpoint when not logged in." | |
269 | msgstr "" | |
270 | ||
271 | #: flask_security/core.py:384 | |
272 | msgid "Failed to send code. Please try again later" | |
273 | msgstr "" | |
274 | ||
275 | #: flask_security/core.py:385 | |
276 | msgid "Invalid Token" | |
277 | msgstr "" | |
278 | ||
279 | #: flask_security/core.py:386 | |
280 | msgid "Your token has been confirmed" | |
281 | msgstr "" | |
282 | ||
283 | #: flask_security/core.py:388 | |
284 | msgid "You successfully changed your two-factor method." | |
285 | msgstr "" | |
286 | ||
287 | #: flask_security/core.py:392 | |
288 | msgid "You successfully confirmed password" | |
289 | msgstr "" | |
290 | ||
291 | #: flask_security/core.py:396 | |
292 | msgid "Password confirmation is needed in order to access page" | |
293 | msgstr "" | |
294 | ||
295 | #: flask_security/core.py:400 | |
296 | msgid "You currently do not have permissions to access this page" | |
297 | msgstr "" | |
298 | ||
299 | #: flask_security/core.py:403 | |
300 | msgid "Marked method is not valid" | |
301 | msgstr "" | |
302 | ||
303 | #: flask_security/core.py:405 | |
304 | msgid "You successfully disabled two factor authorization." | |
305 | msgstr "" | |
306 | ||
307 | #: flask_security/core.py:408 | |
308 | msgid "Requested method is not valid" | |
309 | msgstr "" | |
310 | ||
311 | #: flask_security/core.py:410 | |
312 | #, python-format | |
313 | msgid "Setup must be completed within %(within)s. Please start over." | |
314 | msgstr "" | |
315 | ||
316 | #: flask_security/core.py:413 | |
317 | msgid "Unified sign in setup successful" | |
318 | msgstr "" | |
319 | ||
320 | #: flask_security/core.py:414 | |
321 | msgid "You must specify a valid identity to sign in" | |
322 | msgstr "" | |
323 | ||
324 | #: flask_security/core.py:415 | |
325 | #, python-format | |
326 | msgid "Use this code to sign in: %(code)s." | |
327 | msgstr "" | |
328 | ||
329 | #: flask_security/forms.py:50 | |
330 | msgid "Email Address" | |
331 | msgstr "Correu electrònic" | |
332 | ||
333 | #: flask_security/forms.py:51 | |
334 | msgid "Password" | |
335 | msgstr "Contrasenya" | |
336 | ||
337 | #: flask_security/forms.py:52 | |
338 | msgid "Remember Me" | |
339 | msgstr "Recorda'm" | |
340 | ||
341 | #: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 | |
342 | #: flask_security/templates/security/login_user.html:6 | |
343 | #: flask_security/templates/security/send_login.html:6 | |
344 | msgid "Login" | |
345 | msgstr "Iniciar sessió" | |
346 | ||
347 | #: flask_security/forms.py:54 | |
348 | #: flask_security/templates/security/email/us_instructions.html:8 | |
349 | #: flask_security/templates/security/us_signin.html:6 | |
350 | msgid "Sign In" | |
351 | msgstr "" | |
352 | ||
353 | #: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11 | |
354 | #: flask_security/templates/security/register_user.html:6 | |
355 | msgid "Register" | |
356 | msgstr "Registrar-se" | |
357 | ||
358 | #: flask_security/forms.py:56 | |
359 | msgid "Resend Confirmation Instructions" | |
360 | msgstr "Reenviar les instruccions de confirmació" | |
361 | ||
362 | #: flask_security/forms.py:57 | |
363 | msgid "Recover Password" | |
364 | msgstr "Restablir la contrasenya" | |
365 | ||
366 | #: flask_security/forms.py:58 | |
367 | msgid "Reset Password" | |
368 | msgstr "Restablir la contrasenya" | |
369 | ||
370 | #: flask_security/forms.py:59 | |
371 | msgid "Retype Password" | |
372 | msgstr "Escriu la contrasenya una altra vegada" | |
373 | ||
374 | #: flask_security/forms.py:60 | |
375 | msgid "New Password" | |
376 | msgstr "Nova contrasenya" | |
377 | ||
378 | #: flask_security/forms.py:61 | |
379 | msgid "Change Password" | |
380 | msgstr "Canvi de contrasenya" | |
381 | ||
382 | #: flask_security/forms.py:62 | |
383 | msgid "Send Login Link" | |
384 | msgstr "Enviar l'enllaç d'inici de sessió" | |
385 | ||
386 | #: flask_security/forms.py:63 | |
387 | msgid "Verify Password" | |
388 | msgstr "" | |
389 | ||
390 | #: flask_security/forms.py:64 | |
391 | msgid "Change Method" | |
392 | msgstr "" | |
393 | ||
394 | #: flask_security/forms.py:65 | |
395 | msgid "Phone Number" | |
396 | msgstr "" | |
397 | ||
398 | #: flask_security/forms.py:66 | |
399 | msgid "Authentication Code" | |
400 | msgstr "" | |
401 | ||
402 | #: flask_security/forms.py:67 | |
403 | msgid "Submit" | |
404 | msgstr "" | |
405 | ||
406 | #: flask_security/forms.py:68 | |
407 | msgid "Submit Code" | |
408 | msgstr "" | |
409 | ||
410 | #: flask_security/forms.py:69 | |
411 | msgid "Error(s)" | |
412 | msgstr "" | |
413 | ||
414 | #: flask_security/forms.py:70 | |
415 | msgid "Identity" | |
416 | msgstr "" | |
417 | ||
418 | #: flask_security/forms.py:71 | |
419 | msgid "Send Code" | |
420 | msgstr "" | |
421 | ||
422 | #: flask_security/forms.py:72 | |
423 | #, fuzzy | |
424 | msgid "Passcode" | |
425 | msgstr "Contrasenya" | |
426 | ||
427 | #: flask_security/unified_signin.py:145 | |
428 | #, fuzzy | |
429 | msgid "Code or Password" | |
430 | msgstr "Restablir la contrasenya" | |
431 | ||
432 | #: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 | |
433 | msgid "Available Methods" | |
434 | msgstr "" | |
435 | ||
436 | #: flask_security/unified_signin.py:151 | |
437 | msgid "Via email" | |
438 | msgstr "" | |
439 | ||
440 | #: flask_security/unified_signin.py:151 | |
441 | msgid "Via SMS" | |
442 | msgstr "" | |
443 | ||
444 | #: flask_security/unified_signin.py:272 | |
445 | msgid "Set up using email" | |
446 | msgstr "" | |
447 | ||
448 | #: flask_security/unified_signin.py:275 | |
449 | msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" | |
450 | msgstr "" | |
451 | ||
452 | #: flask_security/unified_signin.py:277 | |
453 | msgid "Set up using SMS" | |
454 | msgstr "" | |
455 | ||
456 | #: flask_security/templates/security/_menu.html:2 | |
457 | msgid "Menu" | |
458 | msgstr "Menú" | |
459 | ||
460 | #: flask_security/templates/security/_menu.html:8 | |
461 | msgid "Unified Sign In" | |
462 | msgstr "" | |
463 | ||
464 | #: flask_security/templates/security/_menu.html:14 | |
465 | msgid "Forgot password" | |
466 | msgstr "Contrasenya oblidada" | |
467 | ||
468 | #: flask_security/templates/security/_menu.html:17 | |
469 | msgid "Confirm account" | |
470 | msgstr "Confirmació de compte" | |
471 | ||
472 | #: flask_security/templates/security/change_password.html:6 | |
473 | msgid "Change password" | |
474 | msgstr "Canviar la contrasenya" | |
475 | ||
476 | #: flask_security/templates/security/forgot_password.html:6 | |
477 | msgid "Send password reset instructions" | |
478 | msgstr "Enviar instruccions per restablir la contrasenya" | |
479 | ||
480 | #: flask_security/templates/security/reset_password.html:6 | |
481 | msgid "Reset password" | |
482 | msgstr "Restablir la contrasenya" | |
483 | ||
484 | #: flask_security/templates/security/send_confirmation.html:6 | |
485 | msgid "Resend confirmation instructions" | |
486 | msgstr "Reenviar instruccions de confirmació" | |
487 | ||
488 | #: flask_security/templates/security/two_factor_setup.html:6 | |
489 | msgid "Two-factor authentication adds an extra layer of security to your account" | |
490 | msgstr "" | |
491 | ||
492 | #: flask_security/templates/security/two_factor_setup.html:7 | |
493 | msgid "" | |
494 | "In addition to your username and password, you'll need to use a code that" | |
495 | " we will send you" | |
496 | msgstr "" | |
497 | ||
498 | #: flask_security/templates/security/two_factor_setup.html:18 | |
499 | msgid "To complete logging in, please enter the code sent to your mail" | |
500 | msgstr "" | |
501 | ||
502 | #: flask_security/templates/security/two_factor_setup.html:21 | |
503 | #: flask_security/templates/security/us_setup.html:21 | |
504 | msgid "" | |
505 | "Open your authenticator app on your device and scan the following qrcode " | |
506 | "to start receiving codes:" | |
507 | msgstr "" | |
508 | ||
509 | #: flask_security/templates/security/two_factor_setup.html:22 | |
510 | msgid "Two factor authentication code" | |
511 | msgstr "" | |
512 | ||
513 | #: flask_security/templates/security/two_factor_setup.html:25 | |
514 | msgid "To Which Phone Number Should We Send Code To?" | |
515 | msgstr "" | |
516 | ||
517 | #: flask_security/templates/security/two_factor_verify_code.html:6 | |
518 | msgid "Two-factor Authentication" | |
519 | msgstr "" | |
520 | ||
521 | #: flask_security/templates/security/two_factor_verify_code.html:7 | |
522 | msgid "Please enter your authentication code" | |
523 | msgstr "" | |
524 | ||
525 | #: flask_security/templates/security/two_factor_verify_code.html:18 | |
526 | msgid "The code for authentication was sent to your email address" | |
527 | msgstr "" | |
528 | ||
529 | #: flask_security/templates/security/two_factor_verify_code.html:21 | |
530 | msgid "A mail was sent to us in order to reset your application account" | |
531 | msgstr "" | |
532 | ||
533 | #: flask_security/templates/security/two_factor_verify_password.html:6 | |
534 | #: flask_security/templates/security/verify.html:6 | |
535 | msgid "Please Enter Your Password" | |
536 | msgstr "" | |
537 | ||
538 | #: flask_security/templates/security/us_setup.html:6 | |
539 | msgid "Setup Unified Sign In options" | |
540 | msgstr "" | |
541 | ||
542 | #: flask_security/templates/security/us_setup.html:22 | |
543 | msgid "Passwordless QRCode" | |
544 | msgstr "" | |
545 | ||
546 | #: flask_security/templates/security/us_setup.html:25 | |
547 | #: flask_security/templates/security/us_signin.html:23 | |
548 | #: flask_security/templates/security/us_verify.html:21 | |
549 | #, fuzzy | |
550 | msgid "Code has been sent" | |
551 | msgstr "S'ha restablit la teva contrasenya" | |
552 | ||
553 | #: flask_security/templates/security/us_setup.html:29 | |
554 | msgid "No methods have been enabled - nothing to setup" | |
555 | msgstr "" | |
556 | ||
557 | #: flask_security/templates/security/us_signin.html:15 | |
558 | #: flask_security/templates/security/us_verify.html:13 | |
559 | msgid "Request one-time code be sent" | |
560 | msgstr "" | |
561 | ||
562 | #: flask_security/templates/security/us_verify.html:6 | |
563 | #, fuzzy | |
564 | msgid "Please re-authenticate" | |
565 | msgstr "Has d'iniciar una nova sessió per tal d'accedir a aquesta pàgina." | |
566 | ||
567 | #: flask_security/templates/security/email/change_notice.html:1 | |
568 | msgid "Your password has been changed." | |
569 | msgstr "S'ha canviat la teva contrasenya." | |
570 | ||
571 | #: flask_security/templates/security/email/change_notice.html:3 | |
572 | msgid "If you did not change your password," | |
573 | msgstr "Si no has canviat la teva contrasenya," | |
574 | ||
575 | #: flask_security/templates/security/email/change_notice.html:3 | |
576 | msgid "click here to reset it" | |
577 | msgstr "fes clic aquí per a restablir-la" | |
578 | ||
579 | #: flask_security/templates/security/email/confirmation_instructions.html:1 | |
580 | msgid "Please confirm your email through the link below:" | |
581 | msgstr "Confirma el teu correu electrònic fent clic aquí:" | |
582 | ||
583 | #: flask_security/templates/security/email/confirmation_instructions.html:3 | |
584 | #: flask_security/templates/security/email/welcome.html:6 | |
585 | msgid "Confirm my account" | |
586 | msgstr "Confirmeu el compte" | |
587 | ||
588 | #: flask_security/templates/security/email/login_instructions.html:1 | |
589 | #: flask_security/templates/security/email/welcome.html:1 | |
590 | #, python-format | |
591 | msgid "Welcome %(email)s!" | |
592 | msgstr "Benvingut %(email)s!" | |
593 | ||
594 | #: flask_security/templates/security/email/login_instructions.html:3 | |
595 | msgid "You can log into your account through the link below:" | |
596 | msgstr "Inicia la sessió fent clic aquí:" | |
597 | ||
598 | #: flask_security/templates/security/email/login_instructions.html:5 | |
599 | msgid "Login now" | |
600 | msgstr "Iniciar sessió ara" | |
601 | ||
602 | #: flask_security/templates/security/email/reset_instructions.html:1 | |
603 | msgid "Click here to reset your password" | |
604 | msgstr "Feu clic aquí per restablir la contrasenya" | |
605 | ||
606 | #: flask_security/templates/security/email/two_factor_instructions.html:3 | |
607 | msgid "You can log into your account using the following code:" | |
608 | msgstr "" | |
609 | ||
610 | #: flask_security/templates/security/email/two_factor_rescue.html:1 | |
611 | msgid "can not access mail account" | |
612 | msgstr "" | |
613 | ||
614 | #: flask_security/templates/security/email/us_instructions.html:3 | |
615 | #, fuzzy | |
616 | msgid "You can sign into your account using the following code:" | |
617 | msgstr "Inicia la sessió fent clic aquí:" | |
618 | ||
619 | #: flask_security/templates/security/email/us_instructions.html:6 | |
620 | #, fuzzy | |
621 | msgid "Or use the the link below:" | |
622 | msgstr "Confirmeu el vostre correu electrònic fent clic a continuació:" | |
623 | ||
624 | #: flask_security/templates/security/email/welcome.html:4 | |
625 | msgid "You can confirm your email through the link below:" | |
626 | msgstr "Confirmeu el vostre correu electrònic fent clic a continuació:" | |
627 |
+626
-0
0 | # Danish (Denmark) translations for Flask-Security. | |
1 | # Copyright (C) 2017 ORGANIZATION | |
2 | # This file is distributed under the same license as the Flask-Security | |
3 | # project. | |
4 | # FIRST AUTHOR <EMAIL@ADDRESS>, 2017. | |
5 | # | |
6 | msgid "" | |
7 | msgstr "" | |
8 | "Project-Id-Version: Flask-Security 2.1.0\n" | |
9 | "Report-Msgid-Bugs-To: [email protected]\n" | |
10 | "POT-Creation-Date: 2020-04-19 13:18-0700\n" | |
11 | "PO-Revision-Date: 2017-03-23 14:04+0100\n" | |
12 | "Last-Translator: Leonhard Printz <[email protected]>\n" | |
13 | "Language: da_DK\n" | |
14 | "Language-Team: da_DK <[email protected]>\n" | |
15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" | |
16 | "MIME-Version: 1.0\n" | |
17 | "Content-Type: text/plain; charset=utf-8\n" | |
18 | "Content-Transfer-Encoding: 8bit\n" | |
19 | "Generated-By: Babel 2.8.0\n" | |
20 | ||
21 | #: flask_security/core.py:207 | |
22 | msgid "Login Required" | |
23 | msgstr "Login påkræveet" | |
24 | ||
25 | #: flask_security/core.py:208 | |
26 | #: flask_security/templates/security/email/two_factor_instructions.html:1 | |
27 | #: flask_security/templates/security/email/us_instructions.html:1 | |
28 | msgid "Welcome" | |
29 | msgstr "Velkommen" | |
30 | ||
31 | #: flask_security/core.py:209 | |
32 | msgid "Please confirm your email" | |
33 | msgstr "Bekræft venligst din email" | |
34 | ||
35 | #: flask_security/core.py:210 | |
36 | msgid "Login instructions" | |
37 | msgstr "Logininstruktioner" | |
38 | ||
39 | #: flask_security/core.py:211 | |
40 | #: flask_security/templates/security/email/reset_notice.html:1 | |
41 | msgid "Your password has been reset" | |
42 | msgstr "Din adgangskode er blevet nulstillet" | |
43 | ||
44 | #: flask_security/core.py:212 | |
45 | msgid "Your password has been changed" | |
46 | msgstr "Din adgangskode er blevet ændret" | |
47 | ||
48 | #: flask_security/core.py:213 | |
49 | msgid "Password reset instructions" | |
50 | msgstr "Instruktioner til nulstilling af adganskode" | |
51 | ||
52 | #: flask_security/core.py:216 | |
53 | msgid "Two-factor Login" | |
54 | msgstr "" | |
55 | ||
56 | #: flask_security/core.py:217 | |
57 | msgid "Two-factor Rescue" | |
58 | msgstr "" | |
59 | ||
60 | #: flask_security/core.py:266 | |
61 | msgid "Verification Code" | |
62 | msgstr "" | |
63 | ||
64 | #: flask_security/core.py:282 | |
65 | msgid "Input not appropriate for requested API" | |
66 | msgstr "" | |
67 | ||
68 | #: flask_security/core.py:283 | |
69 | msgid "You do not have permission to view this resource." | |
70 | msgstr "Du har ikke adgang til denne resource." | |
71 | ||
72 | #: flask_security/core.py:285 | |
73 | msgid "You are not authenticated. Please supply the correct credentials." | |
74 | msgstr "" | |
75 | ||
76 | #: flask_security/core.py:289 | |
77 | #, fuzzy | |
78 | msgid "You must re-authenticate to access this endpoint" | |
79 | msgstr "Bekræft identitet for at få adgang til denne side." | |
80 | ||
81 | #: flask_security/core.py:293 | |
82 | #, python-format | |
83 | msgid "Thank you. Confirmation instructions have been sent to %(email)s." | |
84 | msgstr "Mange tak. Bekræftelsesinstruktioner er blevet sendt til %(email)s." | |
85 | ||
86 | #: flask_security/core.py:296 | |
87 | msgid "Thank you. Your email has been confirmed." | |
88 | msgstr "Mange Tak. Din email er blevet bekræftet." | |
89 | ||
90 | #: flask_security/core.py:297 | |
91 | msgid "Your email has already been confirmed." | |
92 | msgstr "Din email er allerede blevet bekræftet." | |
93 | ||
94 | #: flask_security/core.py:298 | |
95 | msgid "Invalid confirmation token." | |
96 | msgstr "Ugyldig bekræftigelsestoken." | |
97 | ||
98 | #: flask_security/core.py:300 | |
99 | #, python-format | |
100 | msgid "%(email)s is already associated with an account." | |
101 | msgstr "%(email)s er allerede brugt af en anden konto." | |
102 | ||
103 | #: flask_security/core.py:303 | |
104 | msgid "Password does not match" | |
105 | msgstr "Adgangskode passer ikke" | |
106 | ||
107 | #: flask_security/core.py:304 | |
108 | msgid "Passwords do not match" | |
109 | msgstr "Adgangskoderne passer ikke" | |
110 | ||
111 | #: flask_security/core.py:305 | |
112 | msgid "Redirections outside the domain are forbidden" | |
113 | msgstr "Omdirigering udenfor domænet er forbudt" | |
114 | ||
115 | #: flask_security/core.py:307 | |
116 | #, python-format | |
117 | msgid "Instructions to reset your password have been sent to %(email)s." | |
118 | msgstr "" | |
119 | "Instruktioner til nulstilling af din adgangskode er blevet sendt til " | |
120 | "%(email)s." | |
121 | ||
122 | #: flask_security/core.py:311 | |
123 | #, python-format | |
124 | msgid "" | |
125 | "You did not reset your password within %(within)s. New instructions have " | |
126 | "been sent to %(email)s." | |
127 | msgstr "" | |
128 | "Du har ikke nulstillet din adgangskode indenfor %(within)s. Nye " | |
129 | "instruktioner er sendt til %(email)s." | |
130 | ||
131 | #: flask_security/core.py:317 | |
132 | msgid "Invalid reset password token." | |
133 | msgstr "Ugyldig nulstillingstoken." | |
134 | ||
135 | #: flask_security/core.py:318 | |
136 | msgid "Email requires confirmation." | |
137 | msgstr "Email kræver bekræftigelse." | |
138 | ||
139 | #: flask_security/core.py:320 | |
140 | #, python-format | |
141 | msgid "Confirmation instructions have been sent to %(email)s." | |
142 | msgstr "Bekræftigelsesinstruktioner er blevet sendt til %(email)s." | |
143 | ||
144 | #: flask_security/core.py:324 | |
145 | #, python-format | |
146 | msgid "" | |
147 | "You did not confirm your email within %(within)s. New instructions to " | |
148 | "confirm your email have been sent to %(email)s." | |
149 | msgstr "" | |
150 | "Du har ikke bekræftet din email indenfor %(within)s. Nye instruktioner er" | |
151 | " blevet sendt til %(email)s." | |
152 | ||
153 | #: flask_security/core.py:332 | |
154 | #, python-format | |
155 | msgid "" | |
156 | "You did not login within %(within)s. New instructions to login have been " | |
157 | "sent to %(email)s." | |
158 | msgstr "" | |
159 | "Du har ikke logget in indenfor %(within)s. Nye logininstruktioner er " | |
160 | "blevet sendt til %(email)s." | |
161 | ||
162 | #: flask_security/core.py:339 | |
163 | #, python-format | |
164 | msgid "Instructions to login have been sent to %(email)s." | |
165 | msgstr "Logininstruktioner er blevet sendt til %(email)s." | |
166 | ||
167 | #: flask_security/core.py:342 | |
168 | msgid "Invalid login token." | |
169 | msgstr "Ugyldig logintoken." | |
170 | ||
171 | #: flask_security/core.py:343 | |
172 | msgid "Account is disabled." | |
173 | msgstr "Kontoen er deaktiveret." | |
174 | ||
175 | #: flask_security/core.py:344 | |
176 | msgid "Email not provided" | |
177 | msgstr "Email ikke angivet" | |
178 | ||
179 | #: flask_security/core.py:345 | |
180 | msgid "Invalid email address" | |
181 | msgstr "Ugyldig email adresse" | |
182 | ||
183 | #: flask_security/core.py:346 | |
184 | #, fuzzy | |
185 | msgid "Invalid code" | |
186 | msgstr "Ugyldig adgangskode" | |
187 | ||
188 | #: flask_security/core.py:347 | |
189 | msgid "Password not provided" | |
190 | msgstr "Adgangskode ikke angivet" | |
191 | ||
192 | #: flask_security/core.py:348 | |
193 | msgid "No password is set for this user" | |
194 | msgstr "Denne bruger har ingen adganskode" | |
195 | ||
196 | #: flask_security/core.py:350 | |
197 | #, fuzzy, python-format | |
198 | msgid "Password must be at least %(length)s characters" | |
199 | msgstr "Adgangskoden skal indeholde mindst 6 tegn" | |
200 | ||
201 | #: flask_security/core.py:353 | |
202 | msgid "Password not complex enough" | |
203 | msgstr "" | |
204 | ||
205 | #: flask_security/core.py:354 | |
206 | msgid "Password on breached list" | |
207 | msgstr "" | |
208 | ||
209 | #: flask_security/core.py:356 | |
210 | msgid "Failed to contact breached passwords site" | |
211 | msgstr "" | |
212 | ||
213 | #: flask_security/core.py:359 | |
214 | msgid "Phone number not valid e.g. missing country code" | |
215 | msgstr "" | |
216 | ||
217 | #: flask_security/core.py:360 | |
218 | msgid "Specified user does not exist" | |
219 | msgstr "Denne bruger findes ikke" | |
220 | ||
221 | #: flask_security/core.py:361 | |
222 | msgid "Invalid password" | |
223 | msgstr "Ugyldig adgangskode" | |
224 | ||
225 | #: flask_security/core.py:362 | |
226 | msgid "Password or code submitted is not valid" | |
227 | msgstr "" | |
228 | ||
229 | #: flask_security/core.py:363 | |
230 | msgid "You have successfully logged in." | |
231 | msgstr "Du er hermed blevet logget ind." | |
232 | ||
233 | #: flask_security/core.py:364 | |
234 | msgid "Forgot password?" | |
235 | msgstr "Glemt adgangskode?" | |
236 | ||
237 | #: flask_security/core.py:366 | |
238 | msgid "" | |
239 | "You successfully reset your password and you have been logged in " | |
240 | "automatically." | |
241 | msgstr "" | |
242 | "Du har hermed nulstillet din adgangskode og er blevet automatisk logget " | |
243 | "ind." | |
244 | ||
245 | #: flask_security/core.py:373 | |
246 | msgid "Your new password must be different than your previous password." | |
247 | msgstr "Din nye adgangskode skal være anderledes end din tidligere adgangskode." | |
248 | ||
249 | #: flask_security/core.py:376 | |
250 | msgid "You successfully changed your password." | |
251 | msgstr "Du har hermed ændret din adgangskode." | |
252 | ||
253 | #: flask_security/core.py:377 | |
254 | msgid "Please log in to access this page." | |
255 | msgstr "Log in for at få adgang til denne side." | |
256 | ||
257 | #: flask_security/core.py:378 | |
258 | msgid "Please reauthenticate to access this page." | |
259 | msgstr "Bekræft identitet for at få adgang til denne side." | |
260 | ||
261 | #: flask_security/core.py:379 | |
262 | msgid "Reauthentication successful" | |
263 | msgstr "" | |
264 | ||
265 | #: flask_security/core.py:381 | |
266 | msgid "You can only access this endpoint when not logged in." | |
267 | msgstr "" | |
268 | ||
269 | #: flask_security/core.py:384 | |
270 | msgid "Failed to send code. Please try again later" | |
271 | msgstr "" | |
272 | ||
273 | #: flask_security/core.py:385 | |
274 | msgid "Invalid Token" | |
275 | msgstr "" | |
276 | ||
277 | #: flask_security/core.py:386 | |
278 | msgid "Your token has been confirmed" | |
279 | msgstr "" | |
280 | ||
281 | #: flask_security/core.py:388 | |
282 | msgid "You successfully changed your two-factor method." | |
283 | msgstr "" | |
284 | ||
285 | #: flask_security/core.py:392 | |
286 | msgid "You successfully confirmed password" | |
287 | msgstr "" | |
288 | ||
289 | #: flask_security/core.py:396 | |
290 | msgid "Password confirmation is needed in order to access page" | |
291 | msgstr "" | |
292 | ||
293 | #: flask_security/core.py:400 | |
294 | msgid "You currently do not have permissions to access this page" | |
295 | msgstr "" | |
296 | ||
297 | #: flask_security/core.py:403 | |
298 | msgid "Marked method is not valid" | |
299 | msgstr "" | |
300 | ||
301 | #: flask_security/core.py:405 | |
302 | msgid "You successfully disabled two factor authorization." | |
303 | msgstr "" | |
304 | ||
305 | #: flask_security/core.py:408 | |
306 | msgid "Requested method is not valid" | |
307 | msgstr "" | |
308 | ||
309 | #: flask_security/core.py:410 | |
310 | #, python-format | |
311 | msgid "Setup must be completed within %(within)s. Please start over." | |
312 | msgstr "" | |
313 | ||
314 | #: flask_security/core.py:413 | |
315 | msgid "Unified sign in setup successful" | |
316 | msgstr "" | |
317 | ||
318 | #: flask_security/core.py:414 | |
319 | msgid "You must specify a valid identity to sign in" | |
320 | msgstr "" | |
321 | ||
322 | #: flask_security/core.py:415 | |
323 | #, python-format | |
324 | msgid "Use this code to sign in: %(code)s." | |
325 | msgstr "" | |
326 | ||
327 | #: flask_security/forms.py:50 | |
328 | msgid "Email Address" | |
329 | msgstr "Email adresse" | |
330 | ||
331 | #: flask_security/forms.py:51 | |
332 | msgid "Password" | |
333 | msgstr "Adgangskode" | |
334 | ||
335 | #: flask_security/forms.py:52 | |
336 | msgid "Remember Me" | |
337 | msgstr "Husk" | |
338 | ||
339 | #: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 | |
340 | #: flask_security/templates/security/login_user.html:6 | |
341 | #: flask_security/templates/security/send_login.html:6 | |
342 | msgid "Login" | |
343 | msgstr "Login" | |
344 | ||
345 | #: flask_security/forms.py:54 | |
346 | #: flask_security/templates/security/email/us_instructions.html:8 | |
347 | #: flask_security/templates/security/us_signin.html:6 | |
348 | msgid "Sign In" | |
349 | msgstr "" | |
350 | ||
351 | #: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11 | |
352 | #: flask_security/templates/security/register_user.html:6 | |
353 | msgid "Register" | |
354 | msgstr "Registrer" | |
355 | ||
356 | #: flask_security/forms.py:56 | |
357 | msgid "Resend Confirmation Instructions" | |
358 | msgstr "Gensend bekræftelsesinstruktioner" | |
359 | ||
360 | #: flask_security/forms.py:57 | |
361 | msgid "Recover Password" | |
362 | msgstr "Genopret adgangskode" | |
363 | ||
364 | #: flask_security/forms.py:58 | |
365 | msgid "Reset Password" | |
366 | msgstr "Nulstil adgangskode" | |
367 | ||
368 | #: flask_security/forms.py:59 | |
369 | msgid "Retype Password" | |
370 | msgstr "Gentast adgangskode" | |
371 | ||
372 | #: flask_security/forms.py:60 | |
373 | msgid "New Password" | |
374 | msgstr "Ny adgangskode" | |
375 | ||
376 | #: flask_security/forms.py:61 | |
377 | msgid "Change Password" | |
378 | msgstr "Ændre adgangskode" | |
379 | ||
380 | #: flask_security/forms.py:62 | |
381 | msgid "Send Login Link" | |
382 | msgstr "Send login link" | |
383 | ||
384 | #: flask_security/forms.py:63 | |
385 | msgid "Verify Password" | |
386 | msgstr "" | |
387 | ||
388 | #: flask_security/forms.py:64 | |
389 | msgid "Change Method" | |
390 | msgstr "" | |
391 | ||
392 | #: flask_security/forms.py:65 | |
393 | msgid "Phone Number" | |
394 | msgstr "" | |
395 | ||
396 | #: flask_security/forms.py:66 | |
397 | msgid "Authentication Code" | |
398 | msgstr "" | |
399 | ||
400 | #: flask_security/forms.py:67 | |
401 | msgid "Submit" | |
402 | msgstr "" | |
403 | ||
404 | #: flask_security/forms.py:68 | |
405 | msgid "Submit Code" | |
406 | msgstr "" | |
407 | ||
408 | #: flask_security/forms.py:69 | |
409 | msgid "Error(s)" | |
410 | msgstr "" | |
411 | ||
412 | #: flask_security/forms.py:70 | |
413 | msgid "Identity" | |
414 | msgstr "" | |
415 | ||
416 | #: flask_security/forms.py:71 | |
417 | msgid "Send Code" | |
418 | msgstr "" | |
419 | ||
420 | #: flask_security/forms.py:72 | |
421 | #, fuzzy | |
422 | msgid "Passcode" | |
423 | msgstr "Adgangskode" | |
424 | ||
425 | #: flask_security/unified_signin.py:145 | |
426 | #, fuzzy | |
427 | msgid "Code or Password" | |
428 | msgstr "Genopret adgangskode" | |
429 | ||
430 | #: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 | |
431 | msgid "Available Methods" | |
432 | msgstr "" | |
433 | ||
434 | #: flask_security/unified_signin.py:151 | |
435 | msgid "Via email" | |
436 | msgstr "" | |
437 | ||
438 | #: flask_security/unified_signin.py:151 | |
439 | msgid "Via SMS" | |
440 | msgstr "" | |
441 | ||
442 | #: flask_security/unified_signin.py:272 | |
443 | msgid "Set up using email" | |
444 | msgstr "" | |
445 | ||
446 | #: flask_security/unified_signin.py:275 | |
447 | msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" | |
448 | msgstr "" | |
449 | ||
450 | #: flask_security/unified_signin.py:277 | |
451 | msgid "Set up using SMS" | |
452 | msgstr "" | |
453 | ||
454 | #: flask_security/templates/security/_menu.html:2 | |
455 | msgid "Menu" | |
456 | msgstr "Menu" | |
457 | ||
458 | #: flask_security/templates/security/_menu.html:8 | |
459 | msgid "Unified Sign In" | |
460 | msgstr "" | |
461 | ||
462 | #: flask_security/templates/security/_menu.html:14 | |
463 | msgid "Forgot password" | |
464 | msgstr "Glemt din adgangskode" | |
465 | ||
466 | #: flask_security/templates/security/_menu.html:17 | |
467 | msgid "Confirm account" | |
468 | msgstr "Bekræft konto" | |
469 | ||
470 | #: flask_security/templates/security/change_password.html:6 | |
471 | msgid "Change password" | |
472 | msgstr "Ændre adgangskode" | |
473 | ||
474 | #: flask_security/templates/security/forgot_password.html:6 | |
475 | msgid "Send password reset instructions" | |
476 | msgstr "Send adgangskode nulstillingsinstruktioner" | |
477 | ||
478 | #: flask_security/templates/security/reset_password.html:6 | |
479 | msgid "Reset password" | |
480 | msgstr "Nulstil adgangskode" | |
481 | ||
482 | #: flask_security/templates/security/send_confirmation.html:6 | |
483 | msgid "Resend confirmation instructions" | |
484 | msgstr "Gensend bekræftelsesinstruktioner" | |
485 | ||
486 | #: flask_security/templates/security/two_factor_setup.html:6 | |
487 | msgid "Two-factor authentication adds an extra layer of security to your account" | |
488 | msgstr "" | |
489 | ||
490 | #: flask_security/templates/security/two_factor_setup.html:7 | |
491 | msgid "" | |
492 | "In addition to your username and password, you'll need to use a code that" | |
493 | " we will send you" | |
494 | msgstr "" | |
495 | ||
496 | #: flask_security/templates/security/two_factor_setup.html:18 | |
497 | msgid "To complete logging in, please enter the code sent to your mail" | |
498 | msgstr "" | |
499 | ||
500 | #: flask_security/templates/security/two_factor_setup.html:21 | |
501 | #: flask_security/templates/security/us_setup.html:21 | |
502 | msgid "" | |
503 | "Open your authenticator app on your device and scan the following qrcode " | |
504 | "to start receiving codes:" | |
505 | msgstr "" | |
506 | ||
507 | #: flask_security/templates/security/two_factor_setup.html:22 | |
508 | msgid "Two factor authentication code" | |
509 | msgstr "" | |
510 | ||
511 | #: flask_security/templates/security/two_factor_setup.html:25 | |
512 | msgid "To Which Phone Number Should We Send Code To?" | |
513 | msgstr "" | |
514 | ||
515 | #: flask_security/templates/security/two_factor_verify_code.html:6 | |
516 | msgid "Two-factor Authentication" | |
517 | msgstr "" | |
518 | ||
519 | #: flask_security/templates/security/two_factor_verify_code.html:7 | |
520 | msgid "Please enter your authentication code" | |
521 | msgstr "" | |
522 | ||
523 | #: flask_security/templates/security/two_factor_verify_code.html:18 | |
524 | msgid "The code for authentication was sent to your email address" | |
525 | msgstr "" | |
526 | ||
527 | #: flask_security/templates/security/two_factor_verify_code.html:21 | |
528 | msgid "A mail was sent to us in order to reset your application account" | |
529 | msgstr "" | |
530 | ||
531 | #: flask_security/templates/security/two_factor_verify_password.html:6 | |
532 | #: flask_security/templates/security/verify.html:6 | |
533 | msgid "Please Enter Your Password" | |
534 | msgstr "" | |
535 | ||
536 | #: flask_security/templates/security/us_setup.html:6 | |
537 | msgid "Setup Unified Sign In options" | |
538 | msgstr "" | |
539 | ||
540 | #: flask_security/templates/security/us_setup.html:22 | |
541 | msgid "Passwordless QRCode" | |
542 | msgstr "" | |
543 | ||
544 | #: flask_security/templates/security/us_setup.html:25 | |
545 | #: flask_security/templates/security/us_signin.html:23 | |
546 | #: flask_security/templates/security/us_verify.html:21 | |
547 | #, fuzzy | |
548 | msgid "Code has been sent" | |
549 | msgstr "Din adgangskode er blevet nulstillet" | |
550 | ||
551 | #: flask_security/templates/security/us_setup.html:29 | |
552 | msgid "No methods have been enabled - nothing to setup" | |
553 | msgstr "" | |
554 | ||
555 | #: flask_security/templates/security/us_signin.html:15 | |
556 | #: flask_security/templates/security/us_verify.html:13 | |
557 | msgid "Request one-time code be sent" | |
558 | msgstr "" | |
559 | ||
560 | #: flask_security/templates/security/us_verify.html:6 | |
561 | #, fuzzy | |
562 | msgid "Please re-authenticate" | |
563 | msgstr "Bekræft identitet for at få adgang til denne side." | |
564 | ||
565 | #: flask_security/templates/security/email/change_notice.html:1 | |
566 | msgid "Your password has been changed." | |
567 | msgstr "Din adgangskode er blevet ændret." | |
568 | ||
569 | #: flask_security/templates/security/email/change_notice.html:3 | |
570 | msgid "If you did not change your password," | |
571 | msgstr "Hvis du ikke har ændret din adgangskode," | |
572 | ||
573 | #: flask_security/templates/security/email/change_notice.html:3 | |
574 | msgid "click here to reset it" | |
575 | msgstr "klik her for at ændre den" | |
576 | ||
577 | #: flask_security/templates/security/email/confirmation_instructions.html:1 | |
578 | msgid "Please confirm your email through the link below:" | |
579 | msgstr "Bekræft venligst din email gennem nedenstående link:" | |
580 | ||
581 | #: flask_security/templates/security/email/confirmation_instructions.html:3 | |
582 | #: flask_security/templates/security/email/welcome.html:6 | |
583 | msgid "Confirm my account" | |
584 | msgstr "Bekræft ny konto" | |
585 | ||
586 | #: flask_security/templates/security/email/login_instructions.html:1 | |
587 | #: flask_security/templates/security/email/welcome.html:1 | |
588 | #, python-format | |
589 | msgid "Welcome %(email)s!" | |
590 | msgstr "Velkommen %(email)s!" | |
591 | ||
592 | #: flask_security/templates/security/email/login_instructions.html:3 | |
593 | msgid "You can log into your account through the link below:" | |
594 | msgstr "Du kan logge ind gennem nedenstående link:" | |
595 | ||
596 | #: flask_security/templates/security/email/login_instructions.html:5 | |
597 | msgid "Login now" | |
598 | msgstr "Login" | |
599 | ||
600 | #: flask_security/templates/security/email/reset_instructions.html:1 | |
601 | msgid "Click here to reset your password" | |
602 | msgstr "Klik her for at nulstille din adgangskode" | |
603 | ||
604 | #: flask_security/templates/security/email/two_factor_instructions.html:3 | |
605 | msgid "You can log into your account using the following code:" | |
606 | msgstr "" | |
607 | ||
608 | #: flask_security/templates/security/email/two_factor_rescue.html:1 | |
609 | msgid "can not access mail account" | |
610 | msgstr "" | |
611 | ||
612 | #: flask_security/templates/security/email/us_instructions.html:3 | |
613 | #, fuzzy | |
614 | msgid "You can sign into your account using the following code:" | |
615 | msgstr "Du kan logge ind gennem nedenstående link:" | |
616 | ||
617 | #: flask_security/templates/security/email/us_instructions.html:6 | |
618 | #, fuzzy | |
619 | msgid "Or use the the link below:" | |
620 | msgstr "Bekræft venligst din email gennem nedenstående link:" | |
621 | ||
622 | #: flask_security/templates/security/email/welcome.html:4 | |
623 | msgid "You can confirm your email through the link below:" | |
624 | msgstr "Bekræft venligst din email gennem nedenstående link:" | |
625 |
+628
-0
0 | # German translation for Flask-Security (Du/Sie distinction has been | |
1 | # avoided) | |
2 | # Copyright (C) 2017 ORGANIZATION | |
3 | # This file is distributed under the same license as the Flask-Security | |
4 | # project. | |
5 | # Ingo Kleiber <[email protected]>, 2017, | |
6 | # Erich Seifert <[email protected]>, 2017. | |
7 | # | |
8 | msgid "" | |
9 | msgstr "" | |
10 | "Project-Id-Version: Flask-Security 2.0.1\n" | |
11 | "Report-Msgid-Bugs-To: [email protected]\n" | |
12 | "POT-Creation-Date: 2020-04-19 13:18-0700\n" | |
13 | "PO-Revision-Date: 2017-09-25 09:14+0200\n" | |
14 | "Last-Translator: Erich Seifert <[email protected]>\n" | |
15 | "Language: de_DE\n" | |
16 | "Language-Team: de_DE <[email protected]>\n" | |
17 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" | |
18 | "MIME-Version: 1.0\n" | |
19 | "Content-Type: text/plain; charset=utf-8\n" | |
20 | "Content-Transfer-Encoding: 8bit\n" | |
21 | "Generated-By: Babel 2.8.0\n" | |
22 | ||
23 | #: flask_security/core.py:207 | |
24 | msgid "Login Required" | |
25 | msgstr "Anmeldung erforderlich" | |
26 | ||
27 | #: flask_security/core.py:208 | |
28 | #: flask_security/templates/security/email/two_factor_instructions.html:1 | |
29 | #: flask_security/templates/security/email/us_instructions.html:1 | |
30 | msgid "Welcome" | |
31 | msgstr "Willkommen" | |
32 | ||
33 | #: flask_security/core.py:209 | |
34 | msgid "Please confirm your email" | |
35 | msgstr "Bitte E-Mail-Adresse bestätigen" | |
36 | ||
37 | #: flask_security/core.py:210 | |
38 | msgid "Login instructions" | |
39 | msgstr "Anmeldeanleitung" | |
40 | ||
41 | #: flask_security/core.py:211 | |
42 | #: flask_security/templates/security/email/reset_notice.html:1 | |
43 | msgid "Your password has been reset" | |
44 | msgstr "Das Passwort wurde zurückgesetzt" | |
45 | ||
46 | #: flask_security/core.py:212 | |
47 | msgid "Your password has been changed" | |
48 | msgstr "Das Passwort wurde geändert" | |
49 | ||
50 | #: flask_security/core.py:213 | |
51 | msgid "Password reset instructions" | |
52 | msgstr "Anleitung zur Passwortwiederherstellung" | |
53 | ||
54 | #: flask_security/core.py:216 | |
55 | msgid "Two-factor Login" | |
56 | msgstr "" | |
57 | ||
58 | #: flask_security/core.py:217 | |
59 | msgid "Two-factor Rescue" | |
60 | msgstr "" | |
61 | ||
62 | #: flask_security/core.py:266 | |
63 | msgid "Verification Code" | |
64 | msgstr "" | |
65 | ||
66 | #: flask_security/core.py:282 | |
67 | msgid "Input not appropriate for requested API" | |
68 | msgstr "" | |
69 | ||
70 | #: flask_security/core.py:283 | |
71 | msgid "You do not have permission to view this resource." | |
72 | msgstr "Keine Berechtigung um diese Ressource zu sehen." | |
73 | ||
74 | #: flask_security/core.py:285 | |
75 | msgid "You are not authenticated. Please supply the correct credentials." | |
76 | msgstr "" | |
77 | ||
78 | #: flask_security/core.py:289 | |
79 | #, fuzzy | |
80 | msgid "You must re-authenticate to access this endpoint" | |
81 | msgstr "Bitte neu authentifizieren, um auf diese Seite zuzugreifen." | |
82 | ||
83 | #: flask_security/core.py:293 | |
84 | #, python-format | |
85 | msgid "Thank you. Confirmation instructions have been sent to %(email)s." | |
86 | msgstr "Vielen Dank. Bestätigungsanleitung wurde an %(email)s gesendet." | |
87 | ||
88 | #: flask_security/core.py:296 | |
89 | msgid "Thank you. Your email has been confirmed." | |
90 | msgstr "Vielen Dank. Die E-Mail-Adresse wurde bestätigt." | |
91 | ||
92 | #: flask_security/core.py:297 | |
93 | msgid "Your email has already been confirmed." | |
94 | msgstr "Die E-Mail-Adresse wurde bereits bestätigt." | |
95 | ||
96 | #: flask_security/core.py:298 | |
97 | msgid "Invalid confirmation token." | |
98 | msgstr "Ungültiger Bestätigungscode." | |
99 | ||
100 | #: flask_security/core.py:300 | |
101 | #, python-format | |
102 | msgid "%(email)s is already associated with an account." | |
103 | msgstr "%(email)s ist bereits mit einem Konto verknüpft." | |
104 | ||
105 | #: flask_security/core.py:303 | |
106 | msgid "Password does not match" | |
107 | msgstr "Das Passwort stimmt nicht überein" | |
108 | ||
109 | #: flask_security/core.py:304 | |
110 | msgid "Passwords do not match" | |
111 | msgstr "Die Passwörter stimmen nicht überein" | |
112 | ||
113 | #: flask_security/core.py:305 | |
114 | msgid "Redirections outside the domain are forbidden" | |
115 | msgstr "Weiterleitungen außerhalb der Domain sind verboten" | |
116 | ||
117 | #: flask_security/core.py:307 | |
118 | #, python-format | |
119 | msgid "Instructions to reset your password have been sent to %(email)s." | |
120 | msgstr "" | |
121 | "Eine Anleitung, um das Passwort wiederherzustellen wurde an %(email)s " | |
122 | "gesendet." | |
123 | ||
124 | #: flask_security/core.py:311 | |
125 | #, python-format | |
126 | msgid "" | |
127 | "You did not reset your password within %(within)s. New instructions have " | |
128 | "been sent to %(email)s." | |
129 | msgstr "" | |
130 | "Das Passwort wurde nicht innerhalb von %(within)s zurückgesetzt. Eine " | |
131 | "neue Anleitung wurde an %(email)s gesendet." | |
132 | ||
133 | #: flask_security/core.py:317 | |
134 | msgid "Invalid reset password token." | |
135 | msgstr "Ungültiger Passwortwiederherstellungscode." | |
136 | ||
137 | #: flask_security/core.py:318 | |
138 | msgid "Email requires confirmation." | |
139 | msgstr "Die E-Mail-Adresse muss bestätigt werden." | |
140 | ||
141 | #: flask_security/core.py:320 | |
142 | #, python-format | |
143 | msgid "Confirmation instructions have been sent to %(email)s." | |
144 | msgstr "Bestätigungsanleitung wurde an %(email)s gesendet." | |
145 | ||
146 | #: flask_security/core.py:324 | |
147 | #, python-format | |
148 | msgid "" | |
149 | "You did not confirm your email within %(within)s. New instructions to " | |
150 | "confirm your email have been sent to %(email)s." | |
151 | msgstr "" | |
152 | "Die E-Mail-Adresse wurden nicht innerhalb von %(within)s bestätigt. Neue " | |
153 | "Instruktionen wurden an %(email)s gesendet." | |
154 | ||
155 | #: flask_security/core.py:332 | |
156 | #, python-format | |
157 | msgid "" | |
158 | "You did not login within %(within)s. New instructions to login have been " | |
159 | "sent to %(email)s." | |
160 | msgstr "" | |
161 | "Die Anmeldung erfolgte nicht in %(within)s. Eine neue Anleitung wurde an " | |
162 | "%(email)s gesendet." | |
163 | ||
164 | #: flask_security/core.py:339 | |
165 | #, python-format | |
166 | msgid "Instructions to login have been sent to %(email)s." | |
167 | msgstr "Eine Anleitung zur Anmeldung wurde an %(email)s gesendet." | |
168 | ||
169 | #: flask_security/core.py:342 | |
170 | msgid "Invalid login token." | |
171 | msgstr "Ungültiger Anmeldecode." | |
172 | ||
173 | #: flask_security/core.py:343 | |
174 | msgid "Account is disabled." | |
175 | msgstr "Konto ist deaktiviert." | |
176 | ||
177 | #: flask_security/core.py:344 | |
178 | msgid "Email not provided" | |
179 | msgstr "Keine E-Mail-Adresse angegeben" | |
180 | ||
181 | #: flask_security/core.py:345 | |
182 | msgid "Invalid email address" | |
183 | msgstr "Ungültige E-Mail-Adresse" | |
184 | ||
185 | #: flask_security/core.py:346 | |
186 | #, fuzzy | |
187 | msgid "Invalid code" | |
188 | msgstr "Ungültiges Passwort" | |
189 | ||
190 | #: flask_security/core.py:347 | |
191 | msgid "Password not provided" | |
192 | msgstr "Kein Passwort angegeben" | |
193 | ||
194 | #: flask_security/core.py:348 | |
195 | msgid "No password is set for this user" | |
196 | msgstr "Für diesen Benutzer ist kein Passwort gesetzt" | |
197 | ||
198 | #: flask_security/core.py:350 | |
199 | #, fuzzy, python-format | |
200 | msgid "Password must be at least %(length)s characters" | |
201 | msgstr "Das Passwort muss mindestens 6 Zeichen lang sein" | |
202 | ||
203 | #: flask_security/core.py:353 | |
204 | msgid "Password not complex enough" | |
205 | msgstr "" | |
206 | ||
207 | #: flask_security/core.py:354 | |
208 | msgid "Password on breached list" | |
209 | msgstr "" | |
210 | ||
211 | #: flask_security/core.py:356 | |
212 | msgid "Failed to contact breached passwords site" | |
213 | msgstr "" | |
214 | ||
215 | #: flask_security/core.py:359 | |
216 | msgid "Phone number not valid e.g. missing country code" | |
217 | msgstr "" | |
218 | ||
219 | #: flask_security/core.py:360 | |
220 | msgid "Specified user does not exist" | |
221 | msgstr "Angegebener Benutzer existiert nicht" | |
222 | ||
223 | #: flask_security/core.py:361 | |
224 | msgid "Invalid password" | |
225 | msgstr "Ungültiges Passwort" | |
226 | ||
227 | #: flask_security/core.py:362 | |
228 | msgid "Password or code submitted is not valid" | |
229 | msgstr "" | |
230 | ||
231 | #: flask_security/core.py:363 | |
232 | msgid "You have successfully logged in." | |
233 | msgstr "Die Anmeldung war erfolgreich." | |
234 | ||
235 | #: flask_security/core.py:364 | |
236 | msgid "Forgot password?" | |
237 | msgstr "Passwort vergessen?" | |
238 | ||
239 | #: flask_security/core.py:366 | |
240 | msgid "" | |
241 | "You successfully reset your password and you have been logged in " | |
242 | "automatically." | |
243 | msgstr "" | |
244 | "Das Passwort wurde erfolgreich wiederhergestellt und die Anmeldung " | |
245 | "erfolgte automatisch." | |
246 | ||
247 | #: flask_security/core.py:373 | |
248 | msgid "Your new password must be different than your previous password." | |
249 | msgstr "Das neue Passwort muss sich vom vorherigen unterscheiden." | |
250 | ||
251 | #: flask_security/core.py:376 | |
252 | msgid "You successfully changed your password." | |
253 | msgstr "Das Passwort wurde erfolgreich geändert." | |
254 | ||
255 | #: flask_security/core.py:377 | |
256 | msgid "Please log in to access this page." | |
257 | msgstr "Bitte anmelden, um diese Seite zu sehen." | |
258 | ||
259 | #: flask_security/core.py:378 | |
260 | msgid "Please reauthenticate to access this page." | |
261 | msgstr "Bitte neu authentifizieren, um auf diese Seite zuzugreifen." | |
262 | ||
263 | #: flask_security/core.py:379 | |
264 | msgid "Reauthentication successful" | |
265 | msgstr "" | |
266 | ||
267 | #: flask_security/core.py:381 | |
268 | msgid "You can only access this endpoint when not logged in." | |
269 | msgstr "" | |
270 | ||
271 | #: flask_security/core.py:384 | |
272 | msgid "Failed to send code. Please try again later" | |
273 | msgstr "" | |
274 | ||
275 | #: flask_security/core.py:385 | |
276 | msgid "Invalid Token" | |
277 | msgstr "" | |
278 | ||
279 | #: flask_security/core.py:386 | |
280 | msgid "Your token has been confirmed" | |
281 | msgstr "" | |
282 | ||
283 | #: flask_security/core.py:388 | |
284 | msgid "You successfully changed your two-factor method." | |
285 | msgstr "" | |
286 | ||
287 | #: flask_security/core.py:392 | |
288 | msgid "You successfully confirmed password" | |
289 | msgstr "" | |
290 | ||
291 | #: flask_security/core.py:396 | |
292 | msgid "Password confirmation is needed in order to access page" | |
293 | msgstr "" | |
294 | ||
295 | #: flask_security/core.py:400 | |
296 | msgid "You currently do not have permissions to access this page" | |
297 | msgstr "" | |
298 | ||
299 | #: flask_security/core.py:403 | |
300 | msgid "Marked method is not valid" | |
301 | msgstr "" | |
302 | ||
303 | #: flask_security/core.py:405 | |
304 | msgid "You successfully disabled two factor authorization." | |
305 | msgstr "" | |
306 | ||
307 | #: flask_security/core.py:408 | |
308 | msgid "Requested method is not valid" | |
309 | msgstr "" | |
310 | ||
311 | #: flask_security/core.py:410 | |
312 | #, python-format | |
313 | msgid "Setup must be completed within %(within)s. Please start over." | |
314 | msgstr "" | |
315 | ||
316 | #: flask_security/core.py:413 | |
317 | msgid "Unified sign in setup successful" | |
318 | msgstr "" | |
319 | ||
320 | #: flask_security/core.py:414 | |
321 | msgid "You must specify a valid identity to sign in" | |
322 | msgstr "" | |
323 | ||
324 | #: flask_security/core.py:415 | |
325 | #, python-format | |
326 | msgid "Use this code to sign in: %(code)s." | |
327 | msgstr "" | |
328 | ||
329 | #: flask_security/forms.py:50 | |
330 | msgid "Email Address" | |
331 | msgstr "E-Mail-Adresse" | |
332 | ||
333 | #: flask_security/forms.py:51 | |
334 | msgid "Password" | |
335 | msgstr "Passwort" | |
336 | ||
337 | #: flask_security/forms.py:52 | |
338 | msgid "Remember Me" | |
339 | msgstr "Erinnern" | |
340 | ||
341 | #: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 | |
342 | #: flask_security/templates/security/login_user.html:6 | |
343 | #: flask_security/templates/security/send_login.html:6 | |
344 | msgid "Login" | |
345 | msgstr "Anmelden" | |
346 | ||
347 | #: flask_security/forms.py:54 | |
348 | #: flask_security/templates/security/email/us_instructions.html:8 | |
349 | #: flask_security/templates/security/us_signin.html:6 | |
350 | msgid "Sign In" | |
351 | msgstr "" | |
352 | ||
353 | #: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11 | |
354 | #: flask_security/templates/security/register_user.html:6 | |
355 | msgid "Register" | |
356 | msgstr "Registrieren" | |
357 | ||
358 | #: flask_security/forms.py:56 | |
359 | msgid "Resend Confirmation Instructions" | |
360 | msgstr "Bestätigungsanleitung neu senden" | |
361 | ||
362 | #: flask_security/forms.py:57 | |
363 | msgid "Recover Password" | |
364 | msgstr "Passwort wiederherstellen" | |
365 | ||
366 | #: flask_security/forms.py:58 | |
367 | msgid "Reset Password" | |
368 | msgstr "Passwort zurücksetzen" | |
369 | ||
370 | #: flask_security/forms.py:59 | |
371 | msgid "Retype Password" | |
372 | msgstr "Passwort neu eingeben" | |
373 | ||
374 | #: flask_security/forms.py:60 | |
375 | msgid "New Password" | |
376 | msgstr "Neues Passwort" | |
377 | ||
378 | #: flask_security/forms.py:61 | |
379 | msgid "Change Password" | |
380 | msgstr "Passwort ändern" | |
381 | ||
382 | #: flask_security/forms.py:62 | |
383 | msgid "Send Login Link" | |
384 | msgstr "Anmelde-Link versenden" | |
385 | ||
386 | #: flask_security/forms.py:63 | |
387 | msgid "Verify Password" | |
388 | msgstr "" | |
389 | ||
390 | #: flask_security/forms.py:64 | |
391 | msgid "Change Method" | |
392 | msgstr "" | |
393 | ||
394 | #: flask_security/forms.py:65 | |
395 | msgid "Phone Number" | |
396 | msgstr "" | |
397 | ||
398 | #: flask_security/forms.py:66 | |
399 | msgid "Authentication Code" | |
400 | msgstr "" | |
401 | ||
402 | #: flask_security/forms.py:67 | |
403 | msgid "Submit" | |
404 | msgstr "" | |
405 | ||
406 | #: flask_security/forms.py:68 | |
407 | msgid "Submit Code" | |
408 | msgstr "" | |
409 | ||
410 | #: flask_security/forms.py:69 | |
411 | msgid "Error(s)" | |
412 | msgstr "" | |
413 | ||
414 | #: flask_security/forms.py:70 | |
415 | msgid "Identity" | |
416 | msgstr "" | |
417 | ||
418 | #: flask_security/forms.py:71 | |
419 | msgid "Send Code" | |
420 | msgstr "" | |
421 | ||
422 | #: flask_security/forms.py:72 | |
423 | #, fuzzy | |
424 | msgid "Passcode" | |
425 | msgstr "Passwort" | |
426 | ||
427 | #: flask_security/unified_signin.py:145 | |
428 | #, fuzzy | |
429 | msgid "Code or Password" | |
430 | msgstr "Passwort wiederherstellen" | |
431 | ||
432 | #: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 | |
433 | msgid "Available Methods" | |
434 | msgstr "" | |
435 | ||
436 | #: flask_security/unified_signin.py:151 | |
437 | msgid "Via email" | |
438 | msgstr "" | |
439 | ||
440 | #: flask_security/unified_signin.py:151 | |
441 | msgid "Via SMS" | |
442 | msgstr "" | |
443 | ||
444 | #: flask_security/unified_signin.py:272 | |
445 | msgid "Set up using email" | |
446 | msgstr "" | |
447 | ||
448 | #: flask_security/unified_signin.py:275 | |
449 | msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" | |
450 | msgstr "" | |
451 | ||
452 | #: flask_security/unified_signin.py:277 | |
453 | msgid "Set up using SMS" | |
454 | msgstr "" | |
455 | ||
456 | #: flask_security/templates/security/_menu.html:2 | |
457 | msgid "Menu" | |
458 | msgstr "Menü" | |
459 | ||
460 | #: flask_security/templates/security/_menu.html:8 | |
461 | msgid "Unified Sign In" | |
462 | msgstr "" | |
463 | ||
464 | #: flask_security/templates/security/_menu.html:14 | |
465 | msgid "Forgot password" | |
466 | msgstr "Passwort vergessen" | |
467 | ||
468 | #: flask_security/templates/security/_menu.html:17 | |
469 | msgid "Confirm account" | |
470 | msgstr "Konto bestätigen" | |
471 | ||
472 | #: flask_security/templates/security/change_password.html:6 | |
473 | msgid "Change password" | |
474 | msgstr "Passwort ändern" | |
475 | ||
476 | #: flask_security/templates/security/forgot_password.html:6 | |
477 | msgid "Send password reset instructions" | |
478 | msgstr "Anleitung zur Passwortzurücksetzung versenden" | |
479 | ||
480 | #: flask_security/templates/security/reset_password.html:6 | |
481 | msgid "Reset password" | |
482 | msgstr "Passwort zurücksetzen" | |
483 | ||
484 | #: flask_security/templates/security/send_confirmation.html:6 | |
485 | msgid "Resend confirmation instructions" | |
486 | msgstr "Bestätigungsanleitung erneut versenden" | |
487 | ||
488 | #: flask_security/templates/security/two_factor_setup.html:6 | |
489 | msgid "Two-factor authentication adds an extra layer of security to your account" | |
490 | msgstr "" | |
491 | ||
492 | #: flask_security/templates/security/two_factor_setup.html:7 | |
493 | msgid "" | |
494 | "In addition to your username and password, you'll need to use a code that" | |
495 | " we will send you" | |
496 | msgstr "" | |
497 | ||
498 | #: flask_security/templates/security/two_factor_setup.html:18 | |
499 | msgid "To complete logging in, please enter the code sent to your mail" | |
500 | msgstr "" | |
501 | ||
502 | #: flask_security/templates/security/two_factor_setup.html:21 | |
503 | #: flask_security/templates/security/us_setup.html:21 | |
504 | msgid "" | |
505 | "Open your authenticator app on your device and scan the following qrcode " | |
506 | "to start receiving codes:" | |
507 | msgstr "" | |
508 | ||
509 | #: flask_security/templates/security/two_factor_setup.html:22 | |
510 | msgid "Two factor authentication code" | |
511 | msgstr "" | |
512 | ||
513 | #: flask_security/templates/security/two_factor_setup.html:25 | |
514 | msgid "To Which Phone Number Should We Send Code To?" | |
515 | msgstr "" | |
516 | ||
517 | #: flask_security/templates/security/two_factor_verify_code.html:6 | |
518 | msgid "Two-factor Authentication" | |
519 | msgstr "" | |
520 | ||
521 | #: flask_security/templates/security/two_factor_verify_code.html:7 | |
522 | msgid "Please enter your authentication code" | |
523 | msgstr "" | |
524 | ||
525 | #: flask_security/templates/security/two_factor_verify_code.html:18 | |
526 | msgid "The code for authentication was sent to your email address" | |
527 | msgstr "" | |
528 | ||
529 | #: flask_security/templates/security/two_factor_verify_code.html:21 | |
530 | msgid "A mail was sent to us in order to reset your application account" | |
531 | msgstr "" | |
532 | ||
533 | #: flask_security/templates/security/two_factor_verify_password.html:6 | |
534 | #: flask_security/templates/security/verify.html:6 | |
535 | msgid "Please Enter Your Password" | |
536 | msgstr "" | |
537 | ||
538 | #: flask_security/templates/security/us_setup.html:6 | |
539 | msgid "Setup Unified Sign In options" | |
540 | msgstr "" | |
541 | ||
542 | #: flask_security/templates/security/us_setup.html:22 | |
543 | msgid "Passwordless QRCode" | |
544 | msgstr "" | |
545 | ||
546 | #: flask_security/templates/security/us_setup.html:25 | |
547 | #: flask_security/templates/security/us_signin.html:23 | |
548 | #: flask_security/templates/security/us_verify.html:21 | |
549 | #, fuzzy | |
550 | msgid "Code has been sent" | |
551 | msgstr "Das Passwort wurde zurückgesetzt" | |
552 | ||
553 | #: flask_security/templates/security/us_setup.html:29 | |
554 | msgid "No methods have been enabled - nothing to setup" | |
555 | msgstr "" | |
556 | ||
557 | #: flask_security/templates/security/us_signin.html:15 | |
558 | #: flask_security/templates/security/us_verify.html:13 | |
559 | msgid "Request one-time code be sent" | |
560 | msgstr "" | |
561 | ||
562 | #: flask_security/templates/security/us_verify.html:6 | |
563 | #, fuzzy | |
564 | msgid "Please re-authenticate" | |
565 | msgstr "Bitte neu authentifizieren, um auf diese Seite zuzugreifen." | |
566 | ||
567 | #: flask_security/templates/security/email/change_notice.html:1 | |
568 | msgid "Your password has been changed." | |
569 | msgstr "Das Passwort wurde geändert." | |
570 | ||
571 | #: flask_security/templates/security/email/change_notice.html:3 | |
572 | msgid "If you did not change your password," | |
573 | msgstr "Falls das Passwort nicht geändert wurde" | |
574 | ||
575 | #: flask_security/templates/security/email/change_notice.html:3 | |
576 | msgid "click here to reset it" | |
577 | msgstr "hier klicken, um es zurückzusetzen" | |
578 | ||
579 | #: flask_security/templates/security/email/confirmation_instructions.html:1 | |
580 | msgid "Please confirm your email through the link below:" | |
581 | msgstr "Bitte die E-Mail-Adresse durch den Link unten bestätigen:" | |
582 | ||
583 | #: flask_security/templates/security/email/confirmation_instructions.html:3 | |
584 | #: flask_security/templates/security/email/welcome.html:6 | |
585 | msgid "Confirm my account" | |
586 | msgstr "Das Konto bestätigen" | |
587 | ||
588 | #: flask_security/templates/security/email/login_instructions.html:1 | |
589 | #: flask_security/templates/security/email/welcome.html:1 | |
590 | #, python-format | |
591 | msgid "Welcome %(email)s!" | |
592 | msgstr "Willkommen %(email)s!" | |
593 | ||
594 | #: flask_security/templates/security/email/login_instructions.html:3 | |
595 | msgid "You can log into your account through the link below:" | |
596 | msgstr "Die Anmeldung kann über den Link unten erfolgen:" | |
597 | ||
598 | #: flask_security/templates/security/email/login_instructions.html:5 | |
599 | msgid "Login now" | |
600 | msgstr "Jetzt anmelden" | |
601 | ||
602 | #: flask_security/templates/security/email/reset_instructions.html:1 | |
603 | msgid "Click here to reset your password" | |
604 | msgstr "Hier klicken, um das Passwort zurückzusetzen" | |
605 | ||
606 | #: flask_security/templates/security/email/two_factor_instructions.html:3 | |
607 | msgid "You can log into your account using the following code:" | |
608 | msgstr "" | |
609 | ||
610 | #: flask_security/templates/security/email/two_factor_rescue.html:1 | |
611 | msgid "can not access mail account" | |
612 | msgstr "" | |
613 | ||
614 | #: flask_security/templates/security/email/us_instructions.html:3 | |
615 | #, fuzzy | |
616 | msgid "You can sign into your account using the following code:" | |
617 | msgstr "Die Anmeldung kann über den Link unten erfolgen:" | |
618 | ||
619 | #: flask_security/templates/security/email/us_instructions.html:6 | |
620 | #, fuzzy | |
621 | msgid "Or use the the link below:" | |
622 | msgstr "Die E-Mail-Adresse kann über den Link unten bestätigt werden" | |
623 | ||
624 | #: flask_security/templates/security/email/welcome.html:4 | |
625 | msgid "You can confirm your email through the link below:" | |
626 | msgstr "Die E-Mail-Adresse kann über den Link unten bestätigt werden" | |
627 |
+629
-0
0 | # Spanish (Spain) translations for Flask-Security. | |
1 | # Copyright (C) 2017 DINSIC | |
2 | # This file is distributed under the same license as the Flask-Security | |
3 | # project. | |
4 | # Mauko Quiroga <[email protected]>, 2017. | |
5 | # | |
6 | msgid "" | |
7 | msgstr "" | |
8 | "Project-Id-Version: Flask-Security 2.0.1\n" | |
9 | "Report-Msgid-Bugs-To: [email protected]\n" | |
10 | "POT-Creation-Date: 2020-04-19 13:18-0700\n" | |
11 | "PO-Revision-Date: 2017-08-25 17:21+0200\n" | |
12 | "Last-Translator: Mauko Quiroga <[email protected]>\n" | |
13 | "Language: es_ES\n" | |
14 | "Language-Team: \n" | |
15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" | |
16 | "MIME-Version: 1.0\n" | |
17 | "Content-Type: text/plain; charset=utf-8\n" | |
18 | "Content-Transfer-Encoding: 8bit\n" | |
19 | "Generated-By: Babel 2.8.0\n" | |
20 | ||
21 | #: flask_security/core.py:207 | |
22 | msgid "Login Required" | |
23 | msgstr "Inicio de sesión necesario" | |
24 | ||
25 | #: flask_security/core.py:208 | |
26 | #: flask_security/templates/security/email/two_factor_instructions.html:1 | |
27 | #: flask_security/templates/security/email/us_instructions.html:1 | |
28 | msgid "Welcome" | |
29 | msgstr "Bienvenido" | |
30 | ||
31 | #: flask_security/core.py:209 | |
32 | msgid "Please confirm your email" | |
33 | msgstr "Por favor, confirma tu correo electrónico" | |
34 | ||
35 | #: flask_security/core.py:210 | |
36 | msgid "Login instructions" | |
37 | msgstr "Instrucciones para iniciar sesión" | |
38 | ||
39 | #: flask_security/core.py:211 | |
40 | #: flask_security/templates/security/email/reset_notice.html:1 | |
41 | msgid "Your password has been reset" | |
42 | msgstr "Tu contraseña ha sido restablecida" | |
43 | ||
44 | #: flask_security/core.py:212 | |
45 | msgid "Your password has been changed" | |
46 | msgstr "Tu contraseña ha sido cambiada" | |
47 | ||
48 | #: flask_security/core.py:213 | |
49 | msgid "Password reset instructions" | |
50 | msgstr "Instrucciones de recuperación de contraseña" | |
51 | ||
52 | #: flask_security/core.py:216 | |
53 | msgid "Two-factor Login" | |
54 | msgstr "" | |
55 | ||
56 | #: flask_security/core.py:217 | |
57 | msgid "Two-factor Rescue" | |
58 | msgstr "" | |
59 | ||
60 | #: flask_security/core.py:266 | |
61 | msgid "Verification Code" | |
62 | msgstr "" | |
63 | ||
64 | #: flask_security/core.py:282 | |
65 | msgid "Input not appropriate for requested API" | |
66 | msgstr "" | |
67 | ||
68 | #: flask_security/core.py:283 | |
69 | msgid "You do not have permission to view this resource." | |
70 | msgstr "No tienes permiso para consultar este recurso." | |
71 | ||
72 | #: flask_security/core.py:285 | |
73 | msgid "You are not authenticated. Please supply the correct credentials." | |
74 | msgstr "" | |
75 | ||
76 | #: flask_security/core.py:289 | |
77 | #, fuzzy | |
78 | msgid "You must re-authenticate to access this endpoint" | |
79 | msgstr "Deber iniciar sesión nuevamente para poder acceder a esta página." | |
80 | ||
81 | #: flask_security/core.py:293 | |
82 | #, python-format | |
83 | msgid "Thank you. Confirmation instructions have been sent to %(email)s." | |
84 | msgstr "" | |
85 | "Gracias. Un correo con instrucciones sobre cómo confirmar tu cuenta ha " | |
86 | "sido enviado a %(email)s." | |
87 | ||
88 | #: flask_security/core.py:296 | |
89 | msgid "Thank you. Your email has been confirmed." | |
90 | msgstr "Gracias. Tu correo electrónico ha sido confirmado." | |
91 | ||
92 | #: flask_security/core.py:297 | |
93 | msgid "Your email has already been confirmed." | |
94 | msgstr "Tu correo electrónico ya ha sido confirmado." | |
95 | ||
96 | #: flask_security/core.py:298 | |
97 | msgid "Invalid confirmation token." | |
98 | msgstr "Autentificador de confirmación inválido." | |
99 | ||
100 | #: flask_security/core.py:300 | |
101 | #, python-format | |
102 | msgid "%(email)s is already associated with an account." | |
103 | msgstr "%(email)s ya está asociado a una cuenta." | |
104 | ||
105 | #: flask_security/core.py:303 | |
106 | msgid "Password does not match" | |
107 | msgstr "La contraseña no coincide" | |
108 | ||
109 | #: flask_security/core.py:304 | |
110 | msgid "Passwords do not match" | |
111 | msgstr "Las contraseñas no coinciden" | |
112 | ||
113 | #: flask_security/core.py:305 | |
114 | msgid "Redirections outside the domain are forbidden" | |
115 | msgstr "Las redirecciones a sitios web externos están prohibidas" | |
116 | ||
117 | #: flask_security/core.py:307 | |
118 | #, python-format | |
119 | msgid "Instructions to reset your password have been sent to %(email)s." | |
120 | msgstr "" | |
121 | "Las instrucciones para restablecer tu contraseña han sido enviadas a " | |
122 | "%(email)s." | |
123 | ||
124 | #: flask_security/core.py:311 | |
125 | #, python-format | |
126 | msgid "" | |
127 | "You did not reset your password within %(within)s. New instructions have " | |
128 | "been sent to %(email)s." | |
129 | msgstr "" | |
130 | "No restableciste tu contraseña antes de %(within)s. Nuevas instrucciones " | |
131 | "han sido enviadas a %(email)s." | |
132 | ||
133 | #: flask_security/core.py:317 | |
134 | msgid "Invalid reset password token." | |
135 | msgstr "Autentificador de restablecimiento de contraseña inválido." | |
136 | ||
137 | #: flask_security/core.py:318 | |
138 | msgid "Email requires confirmation." | |
139 | msgstr "El correo electrónico requiere confirmación." | |
140 | ||
141 | #: flask_security/core.py:320 | |
142 | #, python-format | |
143 | msgid "Confirmation instructions have been sent to %(email)s." | |
144 | msgstr "Las instrucciones de confirmación se han enviado a %(email)s." | |
145 | ||
146 | #: flask_security/core.py:324 | |
147 | #, python-format | |
148 | msgid "" | |
149 | "You did not confirm your email within %(within)s. New instructions to " | |
150 | "confirm your email have been sent to %(email)s." | |
151 | msgstr "" | |
152 | "No confirmaste tu correo electrónico antes de %(within)s. Nuevas " | |
153 | "instrucciones para confirmar tu correo electrónico han sido enviadas a " | |
154 | "%(email)s." | |
155 | ||
156 | #: flask_security/core.py:332 | |
157 | #, python-format | |
158 | msgid "" | |
159 | "You did not login within %(within)s. New instructions to login have been " | |
160 | "sent to %(email)s." | |
161 | msgstr "" | |
162 | "No iniciaste sesión antes de %(within)s. Nuevas instrucciones para " | |
163 | "iniciar sesión han sido enviadas a %(email)s." | |
164 | ||
165 | #: flask_security/core.py:339 | |
166 | #, python-format | |
167 | msgid "Instructions to login have been sent to %(email)s." | |
168 | msgstr "Instrucciones para iniciar sesión han sido enviadas a %(email)s." | |
169 | ||
170 | #: flask_security/core.py:342 | |
171 | msgid "Invalid login token." | |
172 | msgstr "Autenticador de inicio de sesión inválido." | |
173 | ||
174 | #: flask_security/core.py:343 | |
175 | msgid "Account is disabled." | |
176 | msgstr "Cuenta deshabilitada." | |
177 | ||
178 | #: flask_security/core.py:344 | |
179 | msgid "Email not provided" | |
180 | msgstr "Correo electrónico no indicado" | |
181 | ||
182 | #: flask_security/core.py:345 | |
183 | msgid "Invalid email address" | |
184 | msgstr "Dirección de correo electrónico inválida" | |
185 | ||
186 | #: flask_security/core.py:346 | |
187 | #, fuzzy | |
188 | msgid "Invalid code" | |
189 | msgstr "Contraseña inválida" | |
190 | ||
191 | #: flask_security/core.py:347 | |
192 | msgid "Password not provided" | |
193 | msgstr "Contraseña no indicada" | |
194 | ||
195 | #: flask_security/core.py:348 | |
196 | msgid "No password is set for this user" | |
197 | msgstr "Ninguna contraseña ha sido definida para este·a usuario·a" | |
198 | ||
199 | #: flask_security/core.py:350 | |
200 | #, fuzzy, python-format | |
201 | msgid "Password must be at least %(length)s characters" | |
202 | msgstr "La contraseña debe contar al menos con 6 caracteres" | |
203 | ||
204 | #: flask_security/core.py:353 | |
205 | msgid "Password not complex enough" | |
206 | msgstr "" | |
207 | ||
208 | #: flask_security/core.py:354 | |
209 | msgid "Password on breached list" | |
210 | msgstr "" | |
211 | ||
212 | #: flask_security/core.py:356 | |
213 | msgid "Failed to contact breached passwords site" | |
214 | msgstr "" | |
215 | ||
216 | #: flask_security/core.py:359 | |
217 | msgid "Phone number not valid e.g. missing country code" | |
218 | msgstr "" | |
219 | ||
220 | #: flask_security/core.py:360 | |
221 | msgid "Specified user does not exist" | |
222 | msgstr "Usuario·a especificado·a no existe" | |
223 | ||
224 | #: flask_security/core.py:361 | |
225 | msgid "Invalid password" | |
226 | msgstr "Contraseña inválida" | |
227 | ||
228 | #: flask_security/core.py:362 | |
229 | msgid "Password or code submitted is not valid" | |
230 | msgstr "" | |
231 | ||
232 | #: flask_security/core.py:363 | |
233 | msgid "You have successfully logged in." | |
234 | msgstr "Has iniciado sesión con éxito." | |
235 | ||
236 | #: flask_security/core.py:364 | |
237 | msgid "Forgot password?" | |
238 | msgstr "¿Has olvidado tu contraseña?" | |
239 | ||
240 | #: flask_security/core.py:366 | |
241 | msgid "" | |
242 | "You successfully reset your password and you have been logged in " | |
243 | "automatically." | |
244 | msgstr "" | |
245 | "Has restablecido tu contraseña con éxito y has iniciado sesión " | |
246 | "automáticamente." | |
247 | ||
248 | #: flask_security/core.py:373 | |
249 | msgid "Your new password must be different than your previous password." | |
250 | msgstr "Tu nueva contraseña debe ser diferente de la antigua." | |
251 | ||
252 | #: flask_security/core.py:376 | |
253 | msgid "You successfully changed your password." | |
254 | msgstr "Has cambiado tu contraseña con éxito." | |
255 | ||
256 | #: flask_security/core.py:377 | |
257 | msgid "Please log in to access this page." | |
258 | msgstr "Debes iniciar sesión para poder acceder a esta página." | |
259 | ||
260 | #: flask_security/core.py:378 | |
261 | msgid "Please reauthenticate to access this page." | |
262 | msgstr "Deber iniciar sesión nuevamente para poder acceder a esta página." | |
263 | ||
264 | #: flask_security/core.py:379 | |
265 | msgid "Reauthentication successful" | |
266 | msgstr "" | |
267 | ||
268 | #: flask_security/core.py:381 | |
269 | msgid "You can only access this endpoint when not logged in." | |
270 | msgstr "" | |
271 | ||
272 | #: flask_security/core.py:384 | |
273 | msgid "Failed to send code. Please try again later" | |
274 | msgstr "" | |
275 | ||
276 | #: flask_security/core.py:385 | |
277 | msgid "Invalid Token" | |
278 | msgstr "" | |
279 | ||
280 | #: flask_security/core.py:386 | |
281 | msgid "Your token has been confirmed" | |
282 | msgstr "" | |
283 | ||
284 | #: flask_security/core.py:388 | |
285 | msgid "You successfully changed your two-factor method." | |
286 | msgstr "" | |
287 | ||
288 | #: flask_security/core.py:392 | |
289 | msgid "You successfully confirmed password" | |
290 | msgstr "" | |
291 | ||
292 | #: flask_security/core.py:396 | |
293 | msgid "Password confirmation is needed in order to access page" | |
294 | msgstr "" | |
295 | ||
296 | #: flask_security/core.py:400 | |
297 | msgid "You currently do not have permissions to access this page" | |
298 | msgstr "" | |
299 | ||
300 | #: flask_security/core.py:403 | |
301 | msgid "Marked method is not valid" | |
302 | msgstr "" | |
303 | ||
304 | #: flask_security/core.py:405 | |
305 | msgid "You successfully disabled two factor authorization." | |
306 | msgstr "" | |
307 | ||
308 | #: flask_security/core.py:408 | |
309 | msgid "Requested method is not valid" | |
310 | msgstr "" | |
311 | ||
312 | #: flask_security/core.py:410 | |
313 | #, python-format | |
314 | msgid "Setup must be completed within %(within)s. Please start over." | |
315 | msgstr "" | |
316 | ||
317 | #: flask_security/core.py:413 | |
318 | msgid "Unified sign in setup successful" | |
319 | msgstr "" | |
320 | ||
321 | #: flask_security/core.py:414 | |
322 | msgid "You must specify a valid identity to sign in" | |
323 | msgstr "" | |
324 | ||
325 | #: flask_security/core.py:415 | |
326 | #, python-format | |
327 | msgid "Use this code to sign in: %(code)s." | |
328 | msgstr "" | |
329 | ||
330 | #: flask_security/forms.py:50 | |
331 | msgid "Email Address" | |
332 | msgstr "Correo electrónico" | |
333 | ||
334 | #: flask_security/forms.py:51 | |
335 | msgid "Password" | |
336 | msgstr "Contraseña" | |
337 | ||
338 | #: flask_security/forms.py:52 | |
339 | msgid "Remember Me" | |
340 | msgstr "Recordarme" | |
341 | ||
342 | #: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 | |
343 | #: flask_security/templates/security/login_user.html:6 | |
344 | #: flask_security/templates/security/send_login.html:6 | |
345 | msgid "Login" | |
346 | msgstr "Iniciar sesión" | |
347 | ||
348 | #: flask_security/forms.py:54 | |
349 | #: flask_security/templates/security/email/us_instructions.html:8 | |
350 | #: flask_security/templates/security/us_signin.html:6 | |
351 | msgid "Sign In" | |
352 | msgstr "" | |
353 | ||
354 | #: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11 | |
355 | #: flask_security/templates/security/register_user.html:6 | |
356 | msgid "Register" | |
357 | msgstr "Registrarse" | |
358 | ||
359 | #: flask_security/forms.py:56 | |
360 | msgid "Resend Confirmation Instructions" | |
361 | msgstr "Reenviar instrucciones de confirmación" | |
362 | ||
363 | #: flask_security/forms.py:57 | |
364 | msgid "Recover Password" | |
365 | msgstr "Recuperar contraseña" | |
366 | ||
367 | #: flask_security/forms.py:58 | |
368 | msgid "Reset Password" | |
369 | msgstr "Restablecer contraseña" | |
370 | ||
371 | #: flask_security/forms.py:59 | |
372 | msgid "Retype Password" | |
373 | msgstr "Escribir contraseña nuevamente" | |
374 | ||
375 | #: flask_security/forms.py:60 | |
376 | msgid "New Password" | |
377 | msgstr "Nueva contraseña" | |
378 | ||
379 | #: flask_security/forms.py:61 | |
380 | msgid "Change Password" | |
381 | msgstr "Cambiar la contraseña" | |
382 | ||
383 | #: flask_security/forms.py:62 | |
384 | msgid "Send Login Link" | |
385 | msgstr "Enviar enlace para iniciar sesión" | |
386 | ||
387 | #: flask_security/forms.py:63 | |
388 | msgid "Verify Password" | |
389 | msgstr "" | |
390 | ||
391 | #: flask_security/forms.py:64 | |
392 | msgid "Change Method" | |
393 | msgstr "" | |
394 | ||
395 | #: flask_security/forms.py:65 | |
396 | msgid "Phone Number" | |
397 | msgstr "" | |
398 | ||
399 | #: flask_security/forms.py:66 | |
400 | msgid "Authentication Code" | |
401 | msgstr "" | |
402 | ||
403 | #: flask_security/forms.py:67 | |
404 | msgid "Submit" | |
405 | msgstr "" | |
406 | ||
407 | #: flask_security/forms.py:68 | |
408 | msgid "Submit Code" | |
409 | msgstr "" | |
410 | ||
411 | #: flask_security/forms.py:69 | |
412 | msgid "Error(s)" | |
413 | msgstr "" | |
414 | ||
415 | #: flask_security/forms.py:70 | |
416 | msgid "Identity" | |
417 | msgstr "" | |
418 | ||
419 | #: flask_security/forms.py:71 | |
420 | msgid "Send Code" | |
421 | msgstr "" | |
422 | ||
423 | #: flask_security/forms.py:72 | |
424 | #, fuzzy | |
425 | msgid "Passcode" | |
426 | msgstr "Contraseña" | |
427 | ||
428 | #: flask_security/unified_signin.py:145 | |
429 | #, fuzzy | |
430 | msgid "Code or Password" | |
431 | msgstr "Recuperar contraseña" | |
432 | ||
433 | #: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 | |
434 | msgid "Available Methods" | |
435 | msgstr "" | |
436 | ||
437 | #: flask_security/unified_signin.py:151 | |
438 | msgid "Via email" | |
439 | msgstr "" | |
440 | ||
441 | #: flask_security/unified_signin.py:151 | |
442 | msgid "Via SMS" | |
443 | msgstr "" | |
444 | ||
445 | #: flask_security/unified_signin.py:272 | |
446 | msgid "Set up using email" | |
447 | msgstr "" | |
448 | ||
449 | #: flask_security/unified_signin.py:275 | |
450 | msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" | |
451 | msgstr "" | |
452 | ||
453 | #: flask_security/unified_signin.py:277 | |
454 | msgid "Set up using SMS" | |
455 | msgstr "" | |
456 | ||
457 | #: flask_security/templates/security/_menu.html:2 | |
458 | msgid "Menu" | |
459 | msgstr "Menú" | |
460 | ||
461 | #: flask_security/templates/security/_menu.html:8 | |
462 | msgid "Unified Sign In" | |
463 | msgstr "" | |
464 | ||
465 | #: flask_security/templates/security/_menu.html:14 | |
466 | msgid "Forgot password" | |
467 | msgstr "Olvidé mi contraseña" | |
468 | ||
469 | #: flask_security/templates/security/_menu.html:17 | |
470 | msgid "Confirm account" | |
471 | msgstr "Confirmar cuenta" | |
472 | ||
473 | #: flask_security/templates/security/change_password.html:6 | |
474 | msgid "Change password" | |
475 | msgstr "Cambiar la contraseña" | |
476 | ||
477 | #: flask_security/templates/security/forgot_password.html:6 | |
478 | msgid "Send password reset instructions" | |
479 | msgstr "Enviar instrucciones para restablecer la contraseña" | |
480 | ||
481 | #: flask_security/templates/security/reset_password.html:6 | |
482 | msgid "Reset password" | |
483 | msgstr "Restablecer contraseña" | |
484 | ||
485 | #: flask_security/templates/security/send_confirmation.html:6 | |
486 | msgid "Resend confirmation instructions" | |
487 | msgstr "Reenviar instrucciones de confirmación" | |
488 | ||
489 | #: flask_security/templates/security/two_factor_setup.html:6 | |
490 | msgid "Two-factor authentication adds an extra layer of security to your account" | |
491 | msgstr "" | |
492 | ||
493 | #: flask_security/templates/security/two_factor_setup.html:7 | |
494 | msgid "" | |
495 | "In addition to your username and password, you'll need to use a code that" | |
496 | " we will send you" | |
497 | msgstr "" | |
498 | ||
499 | #: flask_security/templates/security/two_factor_setup.html:18 | |
500 | msgid "To complete logging in, please enter the code sent to your mail" | |
501 | msgstr "" | |
502 | ||
503 | #: flask_security/templates/security/two_factor_setup.html:21 | |
504 | #: flask_security/templates/security/us_setup.html:21 | |
505 | msgid "" | |
506 | "Open your authenticator app on your device and scan the following qrcode " | |
507 | "to start receiving codes:" | |
508 | msgstr "" | |
509 | ||
510 | #: flask_security/templates/security/two_factor_setup.html:22 | |
511 | msgid "Two factor authentication code" | |
512 | msgstr "" | |
513 | ||
514 | #: flask_security/templates/security/two_factor_setup.html:25 | |
515 | msgid "To Which Phone Number Should We Send Code To?" | |
516 | msgstr "" | |
517 | ||
518 | #: flask_security/templates/security/two_factor_verify_code.html:6 | |
519 | msgid "Two-factor Authentication" | |
520 | msgstr "" | |
521 | ||
522 | #: flask_security/templates/security/two_factor_verify_code.html:7 | |
523 | msgid "Please enter your authentication code" | |
524 | msgstr "" | |
525 | ||
526 | #: flask_security/templates/security/two_factor_verify_code.html:18 | |
527 | msgid "The code for authentication was sent to your email address" | |
528 | msgstr "" | |
529 | ||
530 | #: flask_security/templates/security/two_factor_verify_code.html:21 | |
531 | msgid "A mail was sent to us in order to reset your application account" | |
532 | msgstr "" | |
533 | ||
534 | #: flask_security/templates/security/two_factor_verify_password.html:6 | |
535 | #: flask_security/templates/security/verify.html:6 | |
536 | msgid "Please Enter Your Password" | |
537 | msgstr "" | |
538 | ||
539 | #: flask_security/templates/security/us_setup.html:6 | |
540 | msgid "Setup Unified Sign In options" | |
541 | msgstr "" | |
542 | ||
543 | #: flask_security/templates/security/us_setup.html:22 | |
544 | msgid "Passwordless QRCode" | |
545 | msgstr "" | |
546 | ||
547 | #: flask_security/templates/security/us_setup.html:25 | |
548 | #: flask_security/templates/security/us_signin.html:23 | |
549 | #: flask_security/templates/security/us_verify.html:21 | |
550 | #, fuzzy | |
551 | msgid "Code has been sent" | |
552 | msgstr "Tu contraseña ha sido restablecida" | |
553 | ||
554 | #: flask_security/templates/security/us_setup.html:29 | |
555 | msgid "No methods have been enabled - nothing to setup" | |
556 | msgstr "" | |
557 | ||
558 | #: flask_security/templates/security/us_signin.html:15 | |
559 | #: flask_security/templates/security/us_verify.html:13 | |
560 | msgid "Request one-time code be sent" | |
561 | msgstr "" | |
562 | ||
563 | #: flask_security/templates/security/us_verify.html:6 | |
564 | #, fuzzy | |
565 | msgid "Please re-authenticate" | |
566 | msgstr "Deber iniciar sesión nuevamente para poder acceder a esta página." | |
567 | ||
568 | #: flask_security/templates/security/email/change_notice.html:1 | |
569 | msgid "Your password has been changed." | |
570 | msgstr "Tu contraseña ha sido cambiada." | |
571 | ||
572 | #: flask_security/templates/security/email/change_notice.html:3 | |
573 | msgid "If you did not change your password," | |
574 | msgstr "Si no has cambiado tu contraseña," | |
575 | ||
576 | #: flask_security/templates/security/email/change_notice.html:3 | |
577 | msgid "click here to reset it" | |
578 | msgstr "haz clic aquí para restablecerla" | |
579 | ||
580 | #: flask_security/templates/security/email/confirmation_instructions.html:1 | |
581 | msgid "Please confirm your email through the link below:" | |
582 | msgstr "Confirma tu correo electrónico haciendo clic aquí:" | |
583 | ||
584 | #: flask_security/templates/security/email/confirmation_instructions.html:3 | |
585 | #: flask_security/templates/security/email/welcome.html:6 | |
586 | msgid "Confirm my account" | |
587 | msgstr "Confirmar mi cuenta" | |
588 | ||
589 | #: flask_security/templates/security/email/login_instructions.html:1 | |
590 | #: flask_security/templates/security/email/welcome.html:1 | |
591 | #, python-format | |
592 | msgid "Welcome %(email)s!" | |
593 | msgstr "¡Bienvenido %(email)s!" | |
594 | ||
595 | #: flask_security/templates/security/email/login_instructions.html:3 | |
596 | msgid "You can log into your account through the link below:" | |
597 | msgstr "Inicia sesión haciendo clic aquí:" | |
598 | ||
599 | #: flask_security/templates/security/email/login_instructions.html:5 | |
600 | msgid "Login now" | |
601 | msgstr "Iniciar sesión ahora" | |
602 | ||
603 | #: flask_security/templates/security/email/reset_instructions.html:1 | |
604 | msgid "Click here to reset your password" | |
605 | msgstr "Haz clic aquí para restablecer la contraseña" | |
606 | ||
607 | #: flask_security/templates/security/email/two_factor_instructions.html:3 | |
608 | msgid "You can log into your account using the following code:" | |
609 | msgstr "" | |
610 | ||
611 | #: flask_security/templates/security/email/two_factor_rescue.html:1 | |
612 | msgid "can not access mail account" | |
613 | msgstr "" | |
614 | ||
615 | #: flask_security/templates/security/email/us_instructions.html:3 | |
616 | #, fuzzy | |
617 | msgid "You can sign into your account using the following code:" | |
618 | msgstr "Inicia sesión haciendo clic aquí:" | |
619 | ||
620 | #: flask_security/templates/security/email/us_instructions.html:6 | |
621 | #, fuzzy | |
622 | msgid "Or use the the link below:" | |
623 | msgstr "Confirma tu correo electrónico haciendo clic aquí:" | |
624 | ||
625 | #: flask_security/templates/security/email/welcome.html:4 | |
626 | msgid "You can confirm your email through the link below:" | |
627 | msgstr "Confirma tu correo electrónico haciendo clic aquí:" | |
628 |
0 | # Translations template for Flask-Security. | |
1 | # Copyright (C) 2020 ORGANIZATION | |
2 | # This file is distributed under the same license as the Flask-Security | |
3 | # project. | |
4 | # FIRST AUTHOR <EMAIL@ADDRESS>, 2020. | |
5 | # | |
6 | #, fuzzy | |
7 | msgid "" | |
8 | msgstr "" | |
9 | "Project-Id-Version: Flask-Security 3.4.0\n" | |
10 | "Report-Msgid-Bugs-To: [email protected]\n" | |
11 | "POT-Creation-Date: 2020-04-19 13:18-0700\n" | |
12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | |
13 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | |
14 | "Language-Team: LANGUAGE <[email protected]>\n" | |
15 | "MIME-Version: 1.0\n" | |
16 | "Content-Type: text/plain; charset=utf-8\n" | |
17 | "Content-Transfer-Encoding: 8bit\n" | |
18 | "Generated-By: Babel 2.8.0\n" | |
19 | ||
20 | #: flask_security/core.py:207 | |
21 | msgid "Login Required" | |
22 | msgstr "" | |
23 | ||
24 | #: flask_security/core.py:208 | |
25 | #: flask_security/templates/security/email/two_factor_instructions.html:1 | |
26 | #: flask_security/templates/security/email/us_instructions.html:1 | |
27 | msgid "Welcome" | |
28 | msgstr "" | |
29 | ||
30 | #: flask_security/core.py:209 | |
31 | msgid "Please confirm your email" | |
32 | msgstr "" | |
33 | ||
34 | #: flask_security/core.py:210 | |
35 | msgid "Login instructions" | |
36 | msgstr "" | |
37 | ||
38 | #: flask_security/core.py:211 | |
39 | #: flask_security/templates/security/email/reset_notice.html:1 | |
40 | msgid "Your password has been reset" | |
41 | msgstr "" | |
42 | ||
43 | #: flask_security/core.py:212 | |
44 | msgid "Your password has been changed" | |
45 | msgstr "" | |
46 | ||
47 | #: flask_security/core.py:213 | |
48 | msgid "Password reset instructions" | |
49 | msgstr "" | |
50 | ||
51 | #: flask_security/core.py:216 | |
52 | msgid "Two-factor Login" | |
53 | msgstr "" | |
54 | ||
55 | #: flask_security/core.py:217 | |
56 | msgid "Two-factor Rescue" | |
57 | msgstr "" | |
58 | ||
59 | #: flask_security/core.py:266 | |
60 | msgid "Verification Code" | |
61 | msgstr "" | |
62 | ||
63 | #: flask_security/core.py:282 | |
64 | msgid "Input not appropriate for requested API" | |
65 | msgstr "" | |
66 | ||
67 | #: flask_security/core.py:283 | |
68 | msgid "You do not have permission to view this resource." | |
69 | msgstr "" | |
70 | ||
71 | #: flask_security/core.py:285 | |
72 | msgid "You are not authenticated. Please supply the correct credentials." | |
73 | msgstr "" | |
74 | ||
75 | #: flask_security/core.py:289 | |
76 | msgid "You must re-authenticate to access this endpoint" | |
77 | msgstr "" | |
78 | ||
79 | #: flask_security/core.py:293 | |
80 | #, python-format | |
81 | msgid "Thank you. Confirmation instructions have been sent to %(email)s." | |
82 | msgstr "" | |
83 | ||
84 | #: flask_security/core.py:296 | |
85 | msgid "Thank you. Your email has been confirmed." | |
86 | msgstr "" | |
87 | ||
88 | #: flask_security/core.py:297 | |
89 | msgid "Your email has already been confirmed." | |
90 | msgstr "" | |
91 | ||
92 | #: flask_security/core.py:298 | |
93 | msgid "Invalid confirmation token." | |
94 | msgstr "" | |
95 | ||
96 | #: flask_security/core.py:300 | |
97 | #, python-format | |
98 | msgid "%(email)s is already associated with an account." | |
99 | msgstr "" | |
100 | ||
101 | #: flask_security/core.py:303 | |
102 | msgid "Password does not match" | |
103 | msgstr "" | |
104 | ||
105 | #: flask_security/core.py:304 | |
106 | msgid "Passwords do not match" | |
107 | msgstr "" | |
108 | ||
109 | #: flask_security/core.py:305 | |
110 | msgid "Redirections outside the domain are forbidden" | |
111 | msgstr "" | |
112 | ||
113 | #: flask_security/core.py:307 | |
114 | #, python-format | |
115 | msgid "Instructions to reset your password have been sent to %(email)s." | |
116 | msgstr "" | |
117 | ||
118 | #: flask_security/core.py:311 | |
119 | #, python-format | |
120 | msgid "" | |
121 | "You did not reset your password within %(within)s. New instructions have " | |
122 | "been sent to %(email)s." | |
123 | msgstr "" | |
124 | ||
125 | #: flask_security/core.py:317 | |
126 | msgid "Invalid reset password token." | |
127 | msgstr "" | |
128 | ||
129 | #: flask_security/core.py:318 | |
130 | msgid "Email requires confirmation." | |
131 | msgstr "" | |
132 | ||
133 | #: flask_security/core.py:320 | |
134 | #, python-format | |
135 | msgid "Confirmation instructions have been sent to %(email)s." | |
136 | msgstr "" | |
137 | ||
138 | #: flask_security/core.py:324 | |
139 | #, python-format | |
140 | msgid "" | |
141 | "You did not confirm your email within %(within)s. New instructions to " | |
142 | "confirm your email have been sent to %(email)s." | |
143 | msgstr "" | |
144 | ||
145 | #: flask_security/core.py:332 | |
146 | #, python-format | |
147 | msgid "" | |
148 | "You did not login within %(within)s. New instructions to login have been " | |
149 | "sent to %(email)s." | |
150 | msgstr "" | |
151 | ||
152 | #: flask_security/core.py:339 | |
153 | #, python-format | |
154 | msgid "Instructions to login have been sent to %(email)s." | |
155 | msgstr "" | |
156 | ||
157 | #: flask_security/core.py:342 | |
158 | msgid "Invalid login token." | |
159 | msgstr "" | |
160 | ||
161 | #: flask_security/core.py:343 | |
162 | msgid "Account is disabled." | |
163 | msgstr "" | |
164 | ||
165 | #: flask_security/core.py:344 | |
166 | msgid "Email not provided" | |
167 | msgstr "" | |
168 | ||
169 | #: flask_security/core.py:345 | |
170 | msgid "Invalid email address" | |
171 | msgstr "" | |
172 | ||
173 | #: flask_security/core.py:346 | |
174 | msgid "Invalid code" | |
175 | msgstr "" | |
176 | ||
177 | #: flask_security/core.py:347 | |
178 | msgid "Password not provided" | |
179 | msgstr "" | |
180 | ||
181 | #: flask_security/core.py:348 | |
182 | msgid "No password is set for this user" | |
183 | msgstr "" | |
184 | ||
185 | #: flask_security/core.py:350 | |
186 | #, python-format | |
187 | msgid "Password must be at least %(length)s characters" | |
188 | msgstr "" | |
189 | ||
190 | #: flask_security/core.py:353 | |
191 | msgid "Password not complex enough" | |
192 | msgstr "" | |
193 | ||
194 | #: flask_security/core.py:354 | |
195 | msgid "Password on breached list" | |
196 | msgstr "" | |
197 | ||
198 | #: flask_security/core.py:356 | |
199 | msgid "Failed to contact breached passwords site" | |
200 | msgstr "" | |
201 | ||
202 | #: flask_security/core.py:359 | |
203 | msgid "Phone number not valid e.g. missing country code" | |
204 | msgstr "" | |
205 | ||
206 | #: flask_security/core.py:360 | |
207 | msgid "Specified user does not exist" | |
208 | msgstr "" | |
209 | ||
210 | #: flask_security/core.py:361 | |
211 | msgid "Invalid password" | |
212 | msgstr "" | |
213 | ||
214 | #: flask_security/core.py:362 | |
215 | msgid "Password or code submitted is not valid" | |
216 | msgstr "" | |
217 | ||
218 | #: flask_security/core.py:363 | |
219 | msgid "You have successfully logged in." | |
220 | msgstr "" | |
221 | ||
222 | #: flask_security/core.py:364 | |
223 | msgid "Forgot password?" | |
224 | msgstr "" | |
225 | ||
226 | #: flask_security/core.py:366 | |
227 | msgid "" | |
228 | "You successfully reset your password and you have been logged in " | |
229 | "automatically." | |
230 | msgstr "" | |
231 | ||
232 | #: flask_security/core.py:373 | |
233 | msgid "Your new password must be different than your previous password." | |
234 | msgstr "" | |
235 | ||
236 | #: flask_security/core.py:376 | |
237 | msgid "You successfully changed your password." | |
238 | msgstr "" | |
239 | ||
240 | #: flask_security/core.py:377 | |
241 | msgid "Please log in to access this page." | |
242 | msgstr "" | |
243 | ||
244 | #: flask_security/core.py:378 | |
245 | msgid "Please reauthenticate to access this page." | |
246 | msgstr "" | |
247 | ||
248 | #: flask_security/core.py:379 | |
249 | msgid "Reauthentication successful" | |
250 | msgstr "" | |
251 | ||
252 | #: flask_security/core.py:381 | |
253 | msgid "You can only access this endpoint when not logged in." | |
254 | msgstr "" | |
255 | ||
256 | #: flask_security/core.py:384 | |
257 | msgid "Failed to send code. Please try again later" | |
258 | msgstr "" | |
259 | ||
260 | #: flask_security/core.py:385 | |
261 | msgid "Invalid Token" | |
262 | msgstr "" | |
263 | ||
264 | #: flask_security/core.py:386 | |
265 | msgid "Your token has been confirmed" | |
266 | msgstr "" | |
267 | ||
268 | #: flask_security/core.py:388 | |
269 | msgid "You successfully changed your two-factor method." | |
270 | msgstr "" | |
271 | ||
272 | #: flask_security/core.py:392 | |
273 | msgid "You successfully confirmed password" | |
274 | msgstr "" | |
275 | ||
276 | #: flask_security/core.py:396 | |
277 | msgid "Password confirmation is needed in order to access page" | |
278 | msgstr "" | |
279 | ||
280 | #: flask_security/core.py:400 | |
281 | msgid "You currently do not have permissions to access this page" | |
282 | msgstr "" | |
283 | ||
284 | #: flask_security/core.py:403 | |
285 | msgid "Marked method is not valid" | |
286 | msgstr "" | |
287 | ||
288 | #: flask_security/core.py:405 | |
289 | msgid "You successfully disabled two factor authorization." | |
290 | msgstr "" | |
291 | ||
292 | #: flask_security/core.py:408 | |
293 | msgid "Requested method is not valid" | |
294 | msgstr "" | |
295 | ||
296 | #: flask_security/core.py:410 | |
297 | #, python-format | |
298 | msgid "Setup must be completed within %(within)s. Please start over." | |
299 | msgstr "" | |
300 | ||
301 | #: flask_security/core.py:413 | |
302 | msgid "Unified sign in setup successful" | |
303 | msgstr "" | |
304 | ||
305 | #: flask_security/core.py:414 | |
306 | msgid "You must specify a valid identity to sign in" | |
307 | msgstr "" | |
308 | ||
309 | #: flask_security/core.py:415 | |
310 | #, python-format | |
311 | msgid "Use this code to sign in: %(code)s." | |
312 | msgstr "" | |
313 | ||
314 | #: flask_security/forms.py:50 | |
315 | msgid "Email Address" | |
316 | msgstr "" | |
317 | ||
318 | #: flask_security/forms.py:51 | |
319 | msgid "Password" | |
320 | msgstr "" | |
321 | ||
322 | #: flask_security/forms.py:52 | |
323 | msgid "Remember Me" | |
324 | msgstr "" | |
325 | ||
326 | #: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 | |
327 | #: flask_security/templates/security/login_user.html:6 | |
328 | #: flask_security/templates/security/send_login.html:6 | |
329 | msgid "Login" | |
330 | msgstr "" | |
331 | ||
332 | #: flask_security/forms.py:54 | |
333 | #: flask_security/templates/security/email/us_instructions.html:8 | |
334 | #: flask_security/templates/security/us_signin.html:6 | |
335 | msgid "Sign In" | |
336 | msgstr "" | |
337 | ||
338 | #: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11 | |
339 | #: flask_security/templates/security/register_user.html:6 | |
340 | msgid "Register" | |
341 | msgstr "" | |
342 | ||
343 | #: flask_security/forms.py:56 | |
344 | msgid "Resend Confirmation Instructions" | |
345 | msgstr "" | |
346 | ||
347 | #: flask_security/forms.py:57 | |
348 | msgid "Recover Password" | |
349 | msgstr "" | |
350 | ||
351 | #: flask_security/forms.py:58 | |
352 | msgid "Reset Password" | |
353 | msgstr "" | |
354 | ||
355 | #: flask_security/forms.py:59 | |
356 | msgid "Retype Password" | |
357 | msgstr "" | |
358 | ||
359 | #: flask_security/forms.py:60 | |
360 | msgid "New Password" | |
361 | msgstr "" | |
362 | ||
363 | #: flask_security/forms.py:61 | |
364 | msgid "Change Password" | |
365 | msgstr "" | |
366 | ||
367 | #: flask_security/forms.py:62 | |
368 | msgid "Send Login Link" | |
369 | msgstr "" | |
370 | ||
371 | #: flask_security/forms.py:63 | |
372 | msgid "Verify Password" | |
373 | msgstr "" | |
374 | ||
375 | #: flask_security/forms.py:64 | |
376 | msgid "Change Method" | |
377 | msgstr "" | |
378 | ||
379 | #: flask_security/forms.py:65 | |
380 | msgid "Phone Number" | |
381 | msgstr "" | |
382 | ||
383 | #: flask_security/forms.py:66 | |
384 | msgid "Authentication Code" | |
385 | msgstr "" | |
386 | ||
387 | #: flask_security/forms.py:67 | |
388 | msgid "Submit" | |
389 | msgstr "" | |
390 | ||
391 | #: flask_security/forms.py:68 | |
392 | msgid "Submit Code" | |
393 | msgstr "" | |
394 | ||
395 | #: flask_security/forms.py:69 | |
396 | msgid "Error(s)" | |
397 | msgstr "" | |
398 | ||
399 | #: flask_security/forms.py:70 | |
400 | msgid "Identity" | |
401 | msgstr "" | |
402 | ||
403 | #: flask_security/forms.py:71 | |
404 | msgid "Send Code" | |
405 | msgstr "" | |
406 | ||
407 | #: flask_security/forms.py:72 | |
408 | msgid "Passcode" | |
409 | msgstr "" | |
410 | ||
411 | #: flask_security/unified_signin.py:145 | |
412 | msgid "Code or Password" | |
413 | msgstr "" | |
414 | ||
415 | #: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 | |
416 | msgid "Available Methods" | |
417 | msgstr "" | |
418 | ||
419 | #: flask_security/unified_signin.py:151 | |
420 | msgid "Via email" | |
421 | msgstr "" | |
422 | ||
423 | #: flask_security/unified_signin.py:151 | |
424 | msgid "Via SMS" | |
425 | msgstr "" | |
426 | ||
427 | #: flask_security/unified_signin.py:272 | |
428 | msgid "Set up using email" | |
429 | msgstr "" | |
430 | ||
431 | #: flask_security/unified_signin.py:275 | |
432 | msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" | |
433 | msgstr "" | |
434 | ||
435 | #: flask_security/unified_signin.py:277 | |
436 | msgid "Set up using SMS" | |
437 | msgstr "" | |
438 | ||
439 | #: flask_security/templates/security/_menu.html:2 | |
440 | msgid "Menu" | |
441 | msgstr "" | |
442 | ||
443 | #: flask_security/templates/security/_menu.html:8 | |
444 | msgid "Unified Sign In" | |
445 | msgstr "" | |
446 | ||
447 | #: flask_security/templates/security/_menu.html:14 | |
448 | msgid "Forgot password" | |
449 | msgstr "" | |
450 | ||
451 | #: flask_security/templates/security/_menu.html:17 | |
452 | msgid "Confirm account" | |
453 | msgstr "" | |
454 | ||
455 | #: flask_security/templates/security/change_password.html:6 | |
456 | msgid "Change password" | |
457 | msgstr "" | |
458 | ||
459 | #: flask_security/templates/security/forgot_password.html:6 | |
460 | msgid "Send password reset instructions" | |
461 | msgstr "" | |
462 | ||
463 | #: flask_security/templates/security/reset_password.html:6 | |
464 | msgid "Reset password" | |
465 | msgstr "" | |
466 | ||
467 | #: flask_security/templates/security/send_confirmation.html:6 | |
468 | msgid "Resend confirmation instructions" | |
469 | msgstr "" | |
470 | ||
471 | #: flask_security/templates/security/two_factor_setup.html:6 | |
472 | msgid "Two-factor authentication adds an extra layer of security to your account" | |
473 | msgstr "" | |
474 | ||
475 | #: flask_security/templates/security/two_factor_setup.html:7 | |
476 | msgid "" | |
477 | "In addition to your username and password, you'll need to use a code that" | |
478 | " we will send you" | |
479 | msgstr "" | |
480 | ||
481 | #: flask_security/templates/security/two_factor_setup.html:18 | |
482 | msgid "To complete logging in, please enter the code sent to your mail" | |
483 | msgstr "" | |
484 | ||
485 | #: flask_security/templates/security/two_factor_setup.html:21 | |
486 | #: flask_security/templates/security/us_setup.html:21 | |
487 | msgid "" | |
488 | "Open your authenticator app on your device and scan the following qrcode " | |
489 | "to start receiving codes:" | |
490 | msgstr "" | |
491 | ||
492 | #: flask_security/templates/security/two_factor_setup.html:22 | |
493 | msgid "Two factor authentication code" | |
494 | msgstr "" | |
495 | ||
496 | #: flask_security/templates/security/two_factor_setup.html:25 | |
497 | msgid "To Which Phone Number Should We Send Code To?" | |
498 | msgstr "" | |
499 | ||
500 | #: flask_security/templates/security/two_factor_verify_code.html:6 | |
501 | msgid "Two-factor Authentication" | |
502 | msgstr "" | |
503 | ||
504 | #: flask_security/templates/security/two_factor_verify_code.html:7 | |
505 | msgid "Please enter your authentication code" | |
506 | msgstr "" | |
507 | ||
508 | #: flask_security/templates/security/two_factor_verify_code.html:18 | |
509 | msgid "The code for authentication was sent to your email address" | |
510 | msgstr "" | |
511 | ||
512 | #: flask_security/templates/security/two_factor_verify_code.html:21 | |
513 | msgid "A mail was sent to us in order to reset your application account" | |
514 | msgstr "" | |
515 | ||
516 | #: flask_security/templates/security/two_factor_verify_password.html:6 | |
517 | #: flask_security/templates/security/verify.html:6 | |
518 | msgid "Please Enter Your Password" | |
519 | msgstr "" | |
520 | ||
521 | #: flask_security/templates/security/us_setup.html:6 | |
522 | msgid "Setup Unified Sign In options" | |
523 | msgstr "" | |
524 | ||
525 | #: flask_security/templates/security/us_setup.html:22 | |
526 | msgid "Passwordless QRCode" | |
527 | msgstr "" | |
528 | ||
529 | #: flask_security/templates/security/us_setup.html:25 | |
530 | #: flask_security/templates/security/us_signin.html:23 | |
531 | #: flask_security/templates/security/us_verify.html:21 | |
532 | msgid "Code has been sent" | |
533 | msgstr "" | |
534 | ||
535 | #: flask_security/templates/security/us_setup.html:29 | |
536 | msgid "No methods have been enabled - nothing to setup" | |
537 | msgstr "" | |
538 | ||
539 | #: flask_security/templates/security/us_signin.html:15 | |
540 | #: flask_security/templates/security/us_verify.html:13 | |
541 | msgid "Request one-time code be sent" | |
542 | msgstr "" | |
543 | ||
544 | #: flask_security/templates/security/us_verify.html:6 | |
545 | msgid "Please re-authenticate" | |
546 | msgstr "" | |
547 | ||
548 | #: flask_security/templates/security/email/change_notice.html:1 | |
549 | msgid "Your password has been changed." | |
550 | msgstr "" | |
551 | ||
552 | #: flask_security/templates/security/email/change_notice.html:3 | |
553 | msgid "If you did not change your password," | |
554 | msgstr "" | |
555 | ||
556 | #: flask_security/templates/security/email/change_notice.html:3 | |
557 | msgid "click here to reset it" | |
558 | msgstr "" | |
559 | ||
560 | #: flask_security/templates/security/email/confirmation_instructions.html:1 | |
561 | msgid "Please confirm your email through the link below:" | |
562 | msgstr "" | |
563 | ||
564 | #: flask_security/templates/security/email/confirmation_instructions.html:3 | |
565 | #: flask_security/templates/security/email/welcome.html:6 | |
566 | msgid "Confirm my account" | |
567 | msgstr "" | |
568 | ||
569 | #: flask_security/templates/security/email/login_instructions.html:1 | |
570 | #: flask_security/templates/security/email/welcome.html:1 | |
571 | #, python-format | |
572 | msgid "Welcome %(email)s!" | |
573 | msgstr "" | |
574 | ||
575 | #: flask_security/templates/security/email/login_instructions.html:3 | |
576 | msgid "You can log into your account through the link below:" | |
577 | msgstr "" | |
578 | ||
579 | #: flask_security/templates/security/email/login_instructions.html:5 | |
580 | msgid "Login now" | |
581 | msgstr "" | |
582 | ||
583 | #: flask_security/templates/security/email/reset_instructions.html:1 | |
584 | msgid "Click here to reset your password" | |
585 | msgstr "" | |
586 | ||
587 | #: flask_security/templates/security/email/two_factor_instructions.html:3 | |
588 | msgid "You can log into your account using the following code:" | |
589 | msgstr "" | |
590 | ||
591 | #: flask_security/templates/security/email/two_factor_rescue.html:1 | |
592 | msgid "can not access mail account" | |
593 | msgstr "" | |
594 | ||
595 | #: flask_security/templates/security/email/us_instructions.html:3 | |
596 | msgid "You can sign into your account using the following code:" | |
597 | msgstr "" | |
598 | ||
599 | #: flask_security/templates/security/email/us_instructions.html:6 | |
600 | msgid "Or use the the link below:" | |
601 | msgstr "" | |
602 | ||
603 | #: flask_security/templates/security/email/welcome.html:4 | |
604 | msgid "You can confirm your email through the link below:" | |
605 | msgstr "" | |
606 |
+630
-0
0 | # French (France) translations for Flask-Security. | |
1 | # Copyright (C) 2017 CERN | |
2 | # This file is distributed under the same license as the Flask-Security | |
3 | # project. | |
4 | # Alexandre Bulté <[email protected]>, 2017. | |
5 | # | |
6 | msgid "" | |
7 | msgstr "" | |
8 | "Project-Id-Version: Flask-Security 2.0.1\n" | |
9 | "Report-Msgid-Bugs-To: [email protected]\n" | |
10 | "POT-Creation-Date: 2020-04-19 13:18-0700\n" | |
11 | "PO-Revision-Date: 2017-06-08 10:13+0200\n" | |
12 | "Last-Translator: Alexandre Bulté <[email protected]>\n" | |
13 | "Language: fr_FR\n" | |
14 | "Language-Team: fr_FR <[email protected]>\n" | |
15 | "Plural-Forms: nplurals=2; plural=(n > 1)\n" | |
16 | "MIME-Version: 1.0\n" | |
17 | "Content-Type: text/plain; charset=utf-8\n" | |
18 | "Content-Transfer-Encoding: 8bit\n" | |
19 | "Generated-By: Babel 2.8.0\n" | |
20 | ||
21 | #: flask_security/core.py:207 | |
22 | msgid "Login Required" | |
23 | msgstr "Connexion requise" | |
24 | ||
25 | #: flask_security/core.py:208 | |
26 | #: flask_security/templates/security/email/two_factor_instructions.html:1 | |
27 | #: flask_security/templates/security/email/us_instructions.html:1 | |
28 | msgid "Welcome" | |
29 | msgstr "Bienvenue" | |
30 | ||
31 | #: flask_security/core.py:209 | |
32 | msgid "Please confirm your email" | |
33 | msgstr "Merci de confirmer votre adresse email" | |
34 | ||
35 | #: flask_security/core.py:210 | |
36 | msgid "Login instructions" | |
37 | msgstr "Instructions de connexion" | |
38 | ||
39 | #: flask_security/core.py:211 | |
40 | #: flask_security/templates/security/email/reset_notice.html:1 | |
41 | msgid "Your password has been reset" | |
42 | msgstr "Votre mot de passe a été réinitialisé" | |
43 | ||
44 | #: flask_security/core.py:212 | |
45 | msgid "Your password has been changed" | |
46 | msgstr "Votre mot de passe a été changé" | |
47 | ||
48 | #: flask_security/core.py:213 | |
49 | msgid "Password reset instructions" | |
50 | msgstr "Instructions de réinitialisation de votre mot de passe" | |
51 | ||
52 | #: flask_security/core.py:216 | |
53 | msgid "Two-factor Login" | |
54 | msgstr "" | |
55 | ||
56 | #: flask_security/core.py:217 | |
57 | msgid "Two-factor Rescue" | |
58 | msgstr "" | |
59 | ||
60 | #: flask_security/core.py:266 | |
61 | msgid "Verification Code" | |
62 | msgstr "" | |
63 | ||
64 | #: flask_security/core.py:282 | |
65 | msgid "Input not appropriate for requested API" | |
66 | msgstr "" | |
67 | ||
68 | #: flask_security/core.py:283 | |
69 | msgid "You do not have permission to view this resource." | |
70 | msgstr "Vous n'avez pas l'autorisation d'accéder à cette ressource." | |
71 | ||
72 | #: flask_security/core.py:285 | |
73 | msgid "You are not authenticated. Please supply the correct credentials." | |
74 | msgstr "" | |
75 | ||
76 | #: flask_security/core.py:289 | |
77 | #, fuzzy | |
78 | msgid "You must re-authenticate to access this endpoint" | |
79 | msgstr "Merci de vous reconnecter pour accéder à cette page." | |
80 | ||
81 | #: flask_security/core.py:293 | |
82 | #, python-format | |
83 | msgid "Thank you. Confirmation instructions have been sent to %(email)s." | |
84 | msgstr "Merci. Les instructions de confirmation ont été envoyées à %(email)s." | |
85 | ||
86 | #: flask_security/core.py:296 | |
87 | msgid "Thank you. Your email has been confirmed." | |
88 | msgstr "Merci. Votre adresse email a été confirmée." | |
89 | ||
90 | #: flask_security/core.py:297 | |
91 | msgid "Your email has already been confirmed." | |
92 | msgstr "Votre adresse email a déjà été confirmée." | |
93 | ||
94 | #: flask_security/core.py:298 | |
95 | msgid "Invalid confirmation token." | |
96 | msgstr "Token de confirmation non valide." | |
97 | ||
98 | #: flask_security/core.py:300 | |
99 | #, python-format | |
100 | msgid "%(email)s is already associated with an account." | |
101 | msgstr "L'adresse %(email)s est déjà utilisée." | |
102 | ||
103 | #: flask_security/core.py:303 | |
104 | msgid "Password does not match" | |
105 | msgstr "Le mot de passe ne correspond pas" | |
106 | ||
107 | #: flask_security/core.py:304 | |
108 | msgid "Passwords do not match" | |
109 | msgstr "Les mots de passe ne correspondent pas" | |
110 | ||
111 | #: flask_security/core.py:305 | |
112 | msgid "Redirections outside the domain are forbidden" | |
113 | msgstr "Les redirections en dehors du domaine sont interdites" | |
114 | ||
115 | #: flask_security/core.py:307 | |
116 | #, python-format | |
117 | msgid "Instructions to reset your password have been sent to %(email)s." | |
118 | msgstr "" | |
119 | "Les instructions de réinitialisation de votre mot de passe ont été " | |
120 | "envoyées à %(email)s." | |
121 | ||
122 | #: flask_security/core.py:311 | |
123 | #, python-format | |
124 | msgid "" | |
125 | "You did not reset your password within %(within)s. New instructions have " | |
126 | "been sent to %(email)s." | |
127 | msgstr "" | |
128 | "Vous n'avez pas réinitialisé votre mot de passe dans l'intervalle requis " | |
129 | "(%(within)s)De nouvelles instructions ont été envoyées à %(email)s." | |
130 | ||
131 | #: flask_security/core.py:317 | |
132 | msgid "Invalid reset password token." | |
133 | msgstr "Token de réinitialisation non valide." | |
134 | ||
135 | #: flask_security/core.py:318 | |
136 | msgid "Email requires confirmation." | |
137 | msgstr "Une confirmation de l'adresse email est requise." | |
138 | ||
139 | #: flask_security/core.py:320 | |
140 | #, python-format | |
141 | msgid "Confirmation instructions have been sent to %(email)s." | |
142 | msgstr "Les instructions de confirmation ont été envoyées à %(email)s." | |
143 | ||
144 | #: flask_security/core.py:324 | |
145 | #, python-format | |
146 | msgid "" | |
147 | "You did not confirm your email within %(within)s. New instructions to " | |
148 | "confirm your email have been sent to %(email)s." | |
149 | msgstr "" | |
150 | "Vous n'avez pas confirmé votre adresse email dans l'intervalle requis " | |
151 | "(%(within)s)De nouvelles instructions ont été envoyées à %(email)s." | |
152 | ||
153 | #: flask_security/core.py:332 | |
154 | #, python-format | |
155 | msgid "" | |
156 | "You did not login within %(within)s. New instructions to login have been " | |
157 | "sent to %(email)s." | |
158 | msgstr "" | |
159 | "Vous ne vous êtes pas connecté dans l'intervalle requis (%(within)s)De " | |
160 | "nouvelles instructions ont été envoyées à %(email)s." | |
161 | ||
162 | #: flask_security/core.py:339 | |
163 | #, python-format | |
164 | msgid "Instructions to login have been sent to %(email)s." | |
165 | msgstr "Les instructions de connexion ont été envoyées à %(email)s." | |
166 | ||
167 | #: flask_security/core.py:342 | |
168 | msgid "Invalid login token." | |
169 | msgstr "Token de connexion non valide." | |
170 | ||
171 | #: flask_security/core.py:343 | |
172 | msgid "Account is disabled." | |
173 | msgstr "Le compte est désactivé." | |
174 | ||
175 | #: flask_security/core.py:344 | |
176 | msgid "Email not provided" | |
177 | msgstr "Merci d'indiquer une adresse email" | |
178 | ||
179 | #: flask_security/core.py:345 | |
180 | msgid "Invalid email address" | |
181 | msgstr "Adresse email non valide" | |
182 | ||
183 | #: flask_security/core.py:346 | |
184 | #, fuzzy | |
185 | msgid "Invalid code" | |
186 | msgstr "Mot de passe non valide" | |
187 | ||
188 | #: flask_security/core.py:347 | |
189 | msgid "Password not provided" | |
190 | msgstr "Merci d'indiquer un mot de passe" | |
191 | ||
192 | #: flask_security/core.py:348 | |
193 | msgid "No password is set for this user" | |
194 | msgstr "Cet utilisateur n'a pas de mot de passe" | |
195 | ||
196 | #: flask_security/core.py:350 | |
197 | #, fuzzy, python-format | |
198 | msgid "Password must be at least %(length)s characters" | |
199 | msgstr "Le mot de passe doit comporter au moins 6 caractères" | |
200 | ||
201 | #: flask_security/core.py:353 | |
202 | msgid "Password not complex enough" | |
203 | msgstr "" | |
204 | ||
205 | #: flask_security/core.py:354 | |
206 | msgid "Password on breached list" | |
207 | msgstr "" | |
208 | ||
209 | #: flask_security/core.py:356 | |
210 | msgid "Failed to contact breached passwords site" | |
211 | msgstr "" | |
212 | ||
213 | #: flask_security/core.py:359 | |
214 | msgid "Phone number not valid e.g. missing country code" | |
215 | msgstr "" | |
216 | ||
217 | #: flask_security/core.py:360 | |
218 | msgid "Specified user does not exist" | |
219 | msgstr "Cet utilisateur n'existe pas" | |
220 | ||
221 | #: flask_security/core.py:361 | |
222 | msgid "Invalid password" | |
223 | msgstr "Mot de passe non valide" | |
224 | ||
225 | #: flask_security/core.py:362 | |
226 | msgid "Password or code submitted is not valid" | |
227 | msgstr "" | |
228 | ||
229 | #: flask_security/core.py:363 | |
230 | msgid "You have successfully logged in." | |
231 | msgstr "Vous êtes bien connecté." | |
232 | ||
233 | #: flask_security/core.py:364 | |
234 | msgid "Forgot password?" | |
235 | msgstr "Mot de passe oublié ?" | |
236 | ||
237 | #: flask_security/core.py:366 | |
238 | msgid "" | |
239 | "You successfully reset your password and you have been logged in " | |
240 | "automatically." | |
241 | msgstr "" | |
242 | "Vous avez bien réinitialisé votre mot de passe et avez été " | |
243 | "automatiquement connecté." | |
244 | ||
245 | #: flask_security/core.py:373 | |
246 | msgid "Your new password must be different than your previous password." | |
247 | msgstr "Votre nouveau mot de passe doit être différent du précédent." | |
248 | ||
249 | #: flask_security/core.py:376 | |
250 | msgid "You successfully changed your password." | |
251 | msgstr "Vous avez bien changé votre mot de passe." | |
252 | ||
253 | #: flask_security/core.py:377 | |
254 | msgid "Please log in to access this page." | |
255 | msgstr "Merci de vous connecter pour accéder à cette page." | |
256 | ||
257 | #: flask_security/core.py:378 | |
258 | msgid "Please reauthenticate to access this page." | |
259 | msgstr "Merci de vous reconnecter pour accéder à cette page." | |
260 | ||
261 | #: flask_security/core.py:379 | |
262 | msgid "Reauthentication successful" | |
263 | msgstr "" | |
264 | ||
265 | #: flask_security/core.py:381 | |
266 | msgid "You can only access this endpoint when not logged in." | |
267 | msgstr "" | |
268 | ||
269 | #: flask_security/core.py:384 | |
270 | msgid "Failed to send code. Please try again later" | |
271 | msgstr "" | |
272 | ||
273 | #: flask_security/core.py:385 | |
274 | msgid "Invalid Token" | |
275 | msgstr "" | |
276 | ||
277 | #: flask_security/core.py:386 | |
278 | msgid "Your token has been confirmed" | |
279 | msgstr "" | |
280 | ||
281 | #: flask_security/core.py:388 | |
282 | msgid "You successfully changed your two-factor method." | |
283 | msgstr "" | |
284 | ||
285 | #: flask_security/core.py:392 | |
286 | msgid "You successfully confirmed password" | |
287 | msgstr "" | |
288 | ||
289 | #: flask_security/core.py:396 | |
290 | msgid "Password confirmation is needed in order to access page" | |
291 | msgstr "" | |
292 | ||
293 | #: flask_security/core.py:400 | |
294 | msgid "You currently do not have permissions to access this page" | |
295 | msgstr "" | |
296 | ||
297 | #: flask_security/core.py:403 | |
298 | msgid "Marked method is not valid" | |
299 | msgstr "" | |
300 | ||
301 | #: flask_security/core.py:405 | |
302 | msgid "You successfully disabled two factor authorization." | |
303 | msgstr "" | |
304 | ||
305 | #: flask_security/core.py:408 | |
306 | msgid "Requested method is not valid" | |
307 | msgstr "" | |
308 | ||
309 | #: flask_security/core.py:410 | |
310 | #, python-format | |
311 | msgid "Setup must be completed within %(within)s. Please start over." | |
312 | msgstr "" | |
313 | ||
314 | #: flask_security/core.py:413 | |
315 | msgid "Unified sign in setup successful" | |
316 | msgstr "" | |
317 | ||
318 | #: flask_security/core.py:414 | |
319 | msgid "You must specify a valid identity to sign in" | |
320 | msgstr "" | |
321 | ||
322 | #: flask_security/core.py:415 | |
323 | #, python-format | |
324 | msgid "Use this code to sign in: %(code)s." | |
325 | msgstr "" | |
326 | ||
327 | #: flask_security/forms.py:50 | |
328 | msgid "Email Address" | |
329 | msgstr "Adresse email" | |
330 | ||
331 | #: flask_security/forms.py:51 | |
332 | msgid "Password" | |
333 | msgstr "Mot de passe" | |
334 | ||
335 | #: flask_security/forms.py:52 | |
336 | msgid "Remember Me" | |
337 | msgstr "Se souvenir de moi" | |
338 | ||
339 | #: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 | |
340 | #: flask_security/templates/security/login_user.html:6 | |
341 | #: flask_security/templates/security/send_login.html:6 | |
342 | msgid "Login" | |
343 | msgstr "Connexion" | |
344 | ||
345 | #: flask_security/forms.py:54 | |
346 | #: flask_security/templates/security/email/us_instructions.html:8 | |
347 | #: flask_security/templates/security/us_signin.html:6 | |
348 | msgid "Sign In" | |
349 | msgstr "" | |
350 | ||
351 | #: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11 | |
352 | #: flask_security/templates/security/register_user.html:6 | |
353 | msgid "Register" | |
354 | msgstr "Inscription" | |
355 | ||
356 | #: flask_security/forms.py:56 | |
357 | msgid "Resend Confirmation Instructions" | |
358 | msgstr "Renvoyer les instructions de confirmation" | |
359 | ||
360 | #: flask_security/forms.py:57 | |
361 | msgid "Recover Password" | |
362 | msgstr "Récupérer le mot de passe" | |
363 | ||
364 | #: flask_security/forms.py:58 | |
365 | msgid "Reset Password" | |
366 | msgstr "Réinitialiser le mot de passe" | |
367 | ||
368 | #: flask_security/forms.py:59 | |
369 | msgid "Retype Password" | |
370 | msgstr "Confirmer le mot de passe" | |
371 | ||
372 | #: flask_security/forms.py:60 | |
373 | msgid "New Password" | |
374 | msgstr "Nouveau mot de passe" | |
375 | ||
376 | #: flask_security/forms.py:61 | |
377 | msgid "Change Password" | |
378 | msgstr "Changer le mot de passe" | |
379 | ||
380 | #: flask_security/forms.py:62 | |
381 | msgid "Send Login Link" | |
382 | msgstr "Envoyer le lien de connexion" | |
383 | ||
384 | #: flask_security/forms.py:63 | |
385 | msgid "Verify Password" | |
386 | msgstr "" | |
387 | ||
388 | #: flask_security/forms.py:64 | |
389 | msgid "Change Method" | |
390 | msgstr "" | |
391 | ||
392 | #: flask_security/forms.py:65 | |
393 | msgid "Phone Number" | |
394 | msgstr "" | |
395 | ||
396 | #: flask_security/forms.py:66 | |
397 | msgid "Authentication Code" | |
398 | msgstr "" | |
399 | ||
400 | #: flask_security/forms.py:67 | |
401 | msgid "Submit" | |
402 | msgstr "" | |
403 | ||
404 | #: flask_security/forms.py:68 | |
405 | msgid "Submit Code" | |
406 | msgstr "" | |
407 | ||
408 | #: flask_security/forms.py:69 | |
409 | msgid "Error(s)" | |
410 | msgstr "" | |
411 | ||
412 | #: flask_security/forms.py:70 | |
413 | msgid "Identity" | |
414 | msgstr "" | |
415 | ||
416 | #: flask_security/forms.py:71 | |
417 | msgid "Send Code" | |
418 | msgstr "" | |
419 | ||
420 | #: flask_security/forms.py:72 | |
421 | #, fuzzy | |
422 | msgid "Passcode" | |
423 | msgstr "Mot de passe" | |
424 | ||
425 | #: flask_security/unified_signin.py:145 | |
426 | #, fuzzy | |
427 | msgid "Code or Password" | |
428 | msgstr "Récupérer le mot de passe" | |
429 | ||
430 | #: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 | |
431 | msgid "Available Methods" | |
432 | msgstr "" | |
433 | ||
434 | #: flask_security/unified_signin.py:151 | |
435 | msgid "Via email" | |
436 | msgstr "" | |
437 | ||
438 | #: flask_security/unified_signin.py:151 | |
439 | msgid "Via SMS" | |
440 | msgstr "" | |
441 | ||
442 | #: flask_security/unified_signin.py:272 | |
443 | msgid "Set up using email" | |
444 | msgstr "" | |
445 | ||
446 | #: flask_security/unified_signin.py:275 | |
447 | msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" | |
448 | msgstr "" | |
449 | ||
450 | #: flask_security/unified_signin.py:277 | |
451 | msgid "Set up using SMS" | |
452 | msgstr "" | |
453 | ||
454 | #: flask_security/templates/security/_menu.html:2 | |
455 | msgid "Menu" | |
456 | msgstr "Menu" | |
457 | ||
458 | #: flask_security/templates/security/_menu.html:8 | |
459 | msgid "Unified Sign In" | |
460 | msgstr "" | |
461 | ||
462 | #: flask_security/templates/security/_menu.html:14 | |
463 | msgid "Forgot password" | |
464 | msgstr "Mot de passe oublié" | |
465 | ||
466 | #: flask_security/templates/security/_menu.html:17 | |
467 | msgid "Confirm account" | |
468 | msgstr "Confirmer le compte" | |
469 | ||
470 | #: flask_security/templates/security/change_password.html:6 | |
471 | msgid "Change password" | |
472 | msgstr "Changer de mot de passe" | |
473 | ||
474 | #: flask_security/templates/security/forgot_password.html:6 | |
475 | msgid "Send password reset instructions" | |
476 | msgstr "Envoyer les instructions de réinitialisation de mot de passe" | |
477 | ||
478 | #: flask_security/templates/security/reset_password.html:6 | |
479 | msgid "Reset password" | |
480 | msgstr "Réinitialiser le mot de passe" | |
481 | ||
482 | #: flask_security/templates/security/send_confirmation.html:6 | |
483 | msgid "Resend confirmation instructions" | |
484 | msgstr "Renvoyer les instructions de confirmation" | |
485 | ||
486 | #: flask_security/templates/security/two_factor_setup.html:6 | |
487 | msgid "Two-factor authentication adds an extra layer of security to your account" | |
488 | msgstr "" | |
489 | ||
490 | #: flask_security/templates/security/two_factor_setup.html:7 | |
491 | msgid "" | |
492 | "In addition to your username and password, you'll need to use a code that" | |
493 | " we will send you" | |
494 | msgstr "" | |
495 | ||
496 | #: flask_security/templates/security/two_factor_setup.html:18 | |
497 | msgid "To complete logging in, please enter the code sent to your mail" | |
498 | msgstr "" | |
499 | ||
500 | #: flask_security/templates/security/two_factor_setup.html:21 | |
501 | #: flask_security/templates/security/us_setup.html:21 | |
502 | msgid "" | |
503 | "Open your authenticator app on your device and scan the following qrcode " | |
504 | "to start receiving codes:" | |
505 | msgstr "" | |
506 | ||
507 | #: flask_security/templates/security/two_factor_setup.html:22 | |
508 | msgid "Two factor authentication code" | |
509 | msgstr "" | |
510 | ||
511 | #: flask_security/templates/security/two_factor_setup.html:25 | |
512 | msgid "To Which Phone Number Should We Send Code To?" | |
513 | msgstr "" | |
514 | ||
515 | #: flask_security/templates/security/two_factor_verify_code.html:6 | |
516 | msgid "Two-factor Authentication" | |
517 | msgstr "" | |
518 | ||
519 | #: flask_security/templates/security/two_factor_verify_code.html:7 | |
520 | msgid "Please enter your authentication code" | |
521 | msgstr "" | |
522 | ||
523 | #: flask_security/templates/security/two_factor_verify_code.html:18 | |
524 | msgid "The code for authentication was sent to your email address" | |
525 | msgstr "" | |
526 | ||
527 | #: flask_security/templates/security/two_factor_verify_code.html:21 | |
528 | msgid "A mail was sent to us in order to reset your application account" | |
529 | msgstr "" | |
530 | ||
531 | #: flask_security/templates/security/two_factor_verify_password.html:6 | |
532 | #: flask_security/templates/security/verify.html:6 | |
533 | msgid "Please Enter Your Password" | |
534 | msgstr "" | |
535 | ||
536 | #: flask_security/templates/security/us_setup.html:6 | |
537 | msgid "Setup Unified Sign In options" | |
538 | msgstr "" | |
539 | ||
540 | #: flask_security/templates/security/us_setup.html:22 | |
541 | msgid "Passwordless QRCode" | |
542 | msgstr "" | |
543 | ||
544 | #: flask_security/templates/security/us_setup.html:25 | |
545 | #: flask_security/templates/security/us_signin.html:23 | |
546 | #: flask_security/templates/security/us_verify.html:21 | |
547 | #, fuzzy | |
548 | msgid "Code has been sent" | |
549 | msgstr "Votre mot de passe a été réinitialisé" | |
550 | ||
551 | #: flask_security/templates/security/us_setup.html:29 | |
552 | msgid "No methods have been enabled - nothing to setup" | |
553 | msgstr "" | |
554 | ||
555 | #: flask_security/templates/security/us_signin.html:15 | |
556 | #: flask_security/templates/security/us_verify.html:13 | |
557 | msgid "Request one-time code be sent" | |
558 | msgstr "" | |
559 | ||
560 | #: flask_security/templates/security/us_verify.html:6 | |
561 | #, fuzzy | |
562 | msgid "Please re-authenticate" | |
563 | msgstr "Merci de vous reconnecter pour accéder à cette page." | |
564 | ||
565 | #: flask_security/templates/security/email/change_notice.html:1 | |
566 | msgid "Your password has been changed." | |
567 | msgstr "Votre mot de passe a été changé." | |
568 | ||
569 | #: flask_security/templates/security/email/change_notice.html:3 | |
570 | msgid "If you did not change your password," | |
571 | msgstr "Si vous n'avez pas changé votre mot de passe," | |
572 | ||
573 | #: flask_security/templates/security/email/change_notice.html:3 | |
574 | msgid "click here to reset it" | |
575 | msgstr "cliquez ici pour le réinitialiser" | |
576 | ||
577 | #: flask_security/templates/security/email/confirmation_instructions.html:1 | |
578 | msgid "Please confirm your email through the link below:" | |
579 | msgstr "Merci de confirmer votre adresse email via le lien ci-dessous :" | |
580 | ||
581 | #: flask_security/templates/security/email/confirmation_instructions.html:3 | |
582 | #: flask_security/templates/security/email/welcome.html:6 | |
583 | msgid "Confirm my account" | |
584 | msgstr "Confirmer mon compte" | |
585 | ||
586 | #: flask_security/templates/security/email/login_instructions.html:1 | |
587 | #: flask_security/templates/security/email/welcome.html:1 | |
588 | #, python-format | |
589 | msgid "Welcome %(email)s!" | |
590 | msgstr "Bienvenue %(email)s !" | |
591 | ||
592 | #: flask_security/templates/security/email/login_instructions.html:3 | |
593 | msgid "You can log into your account through the link below:" | |
594 | msgstr "Vous pouvez vous connecter via le lien ci-dessous :" | |
595 | ||
596 | #: flask_security/templates/security/email/login_instructions.html:5 | |
597 | msgid "Login now" | |
598 | msgstr "Se connecter maintenant" | |
599 | ||
600 | #: flask_security/templates/security/email/reset_instructions.html:1 | |
601 | msgid "Click here to reset your password" | |
602 | msgstr "Cliquez pour réinitialiser votre mot de passe" | |
603 | ||
604 | #: flask_security/templates/security/email/two_factor_instructions.html:3 | |
605 | msgid "You can log into your account using the following code:" | |
606 | msgstr "" | |
607 | ||
608 | #: flask_security/templates/security/email/two_factor_rescue.html:1 | |
609 | msgid "can not access mail account" | |
610 | msgstr "" | |
611 | ||
612 | #: flask_security/templates/security/email/us_instructions.html:3 | |
613 | #, fuzzy | |
614 | msgid "You can sign into your account using the following code:" | |
615 | msgstr "Vous pouvez vous connecter via le lien ci-dessous :" | |
616 | ||
617 | #: flask_security/templates/security/email/us_instructions.html:6 | |
618 | #, fuzzy | |
619 | msgid "Or use the the link below:" | |
620 | msgstr "" | |
621 | "Vous pouvez confirmer votre votre adresse email via le lien ci-" | |
622 | "dessous :" | |
623 | ||
624 | #: flask_security/templates/security/email/welcome.html:4 | |
625 | msgid "You can confirm your email through the link below:" | |
626 | msgstr "" | |
627 | "Vous pouvez confirmer votre votre adresse email via le lien ci-" | |
628 | "dessous :" | |
629 |
+616
-0
0 | # Japanese translations for Flask-Security. | |
1 | # Copyright (C) 2017 CERN | |
2 | # This file is distributed under the same license as the Flask-Security | |
3 | # project. | |
4 | # FIRST AUTHOR <EMAIL@ADDRESS>, 2017. | |
5 | # | |
6 | msgid "" | |
7 | msgstr "" | |
8 | "Project-Id-Version: Flask-Security 2.0.1\n" | |
9 | "Report-Msgid-Bugs-To: [email protected]\n" | |
10 | "POT-Creation-Date: 2020-04-19 13:18-0700\n" | |
11 | "PO-Revision-Date: 2018-01-25 14:12+0900\n" | |
12 | "Last-Translator: \n" | |
13 | "Language: ja\n" | |
14 | "Language-Team: \n" | |
15 | "Plural-Forms: nplurals=1; plural=0\n" | |
16 | "MIME-Version: 1.0\n" | |
17 | "Content-Type: text/plain; charset=utf-8\n" | |
18 | "Content-Transfer-Encoding: 8bit\n" | |
19 | "Generated-By: Babel 2.8.0\n" | |
20 | ||
21 | #: flask_security/core.py:207 | |
22 | msgid "Login Required" | |
23 | msgstr "ログインが必要です" | |
24 | ||
25 | #: flask_security/core.py:208 | |
26 | #: flask_security/templates/security/email/two_factor_instructions.html:1 | |
27 | #: flask_security/templates/security/email/us_instructions.html:1 | |
28 | msgid "Welcome" | |
29 | msgstr "ようこそ" | |
30 | ||
31 | #: flask_security/core.py:209 | |
32 | msgid "Please confirm your email" | |
33 | msgstr "メール アドレスの検証" | |
34 | ||
35 | #: flask_security/core.py:210 | |
36 | msgid "Login instructions" | |
37 | msgstr "ログイン手順" | |
38 | ||
39 | #: flask_security/core.py:211 | |
40 | #: flask_security/templates/security/email/reset_notice.html:1 | |
41 | msgid "Your password has been reset" | |
42 | msgstr "パスワード変更" | |
43 | ||
44 | #: flask_security/core.py:212 | |
45 | msgid "Your password has been changed" | |
46 | msgstr "パスワードが変更されました。" | |
47 | ||
48 | #: flask_security/core.py:213 | |
49 | msgid "Password reset instructions" | |
50 | msgstr "パスワード再設定手順" | |
51 | ||
52 | #: flask_security/core.py:216 | |
53 | msgid "Two-factor Login" | |
54 | msgstr "" | |
55 | ||
56 | #: flask_security/core.py:217 | |
57 | msgid "Two-factor Rescue" | |
58 | msgstr "" | |
59 | ||
60 | #: flask_security/core.py:266 | |
61 | msgid "Verification Code" | |
62 | msgstr "" | |
63 | ||
64 | #: flask_security/core.py:282 | |
65 | msgid "Input not appropriate for requested API" | |
66 | msgstr "" | |
67 | ||
68 | #: flask_security/core.py:283 | |
69 | msgid "You do not have permission to view this resource." | |
70 | msgstr "アクセス権がありません" | |
71 | ||
72 | #: flask_security/core.py:285 | |
73 | msgid "You are not authenticated. Please supply the correct credentials." | |
74 | msgstr "" | |
75 | ||
76 | #: flask_security/core.py:289 | |
77 | #, fuzzy | |
78 | msgid "You must re-authenticate to access this endpoint" | |
79 | msgstr "再度ログインしてください" | |
80 | ||
81 | #: flask_security/core.py:293 | |
82 | #, python-format | |
83 | msgid "Thank you. Confirmation instructions have been sent to %(email)s." | |
84 | msgstr "ご登録ありがとうございます。%(email)sにメール アドレス検証手順が送信されました。" | |
85 | ||
86 | #: flask_security/core.py:296 | |
87 | msgid "Thank you. Your email has been confirmed." | |
88 | msgstr "ありがとうございます。メール アドレスが検証されました。" | |
89 | ||
90 | #: flask_security/core.py:297 | |
91 | msgid "Your email has already been confirmed." | |
92 | msgstr "メール アドレスは検証済みです" | |
93 | ||
94 | #: flask_security/core.py:298 | |
95 | msgid "Invalid confirmation token." | |
96 | msgstr "リンクが無効です" | |
97 | ||
98 | #: flask_security/core.py:300 | |
99 | #, python-format | |
100 | msgid "%(email)s is already associated with an account." | |
101 | msgstr "%(email)s のアカウントは既に作成されています" | |
102 | ||
103 | #: flask_security/core.py:303 | |
104 | msgid "Password does not match" | |
105 | msgstr "パスワードが一致しません" | |
106 | ||
107 | #: flask_security/core.py:304 | |
108 | msgid "Passwords do not match" | |
109 | msgstr "入力したパスワードが一致していません" | |
110 | ||
111 | #: flask_security/core.py:305 | |
112 | msgid "Redirections outside the domain are forbidden" | |
113 | msgstr "ドメイン外へのリダイレクトは禁止されています" | |
114 | ||
115 | #: flask_security/core.py:307 | |
116 | #, python-format | |
117 | msgid "Instructions to reset your password have been sent to %(email)s." | |
118 | msgstr "パスワードの再設定手順が %(email)s に送信されました" | |
119 | ||
120 | #: flask_security/core.py:311 | |
121 | #, python-format | |
122 | msgid "" | |
123 | "You did not reset your password within %(within)s. New instructions have " | |
124 | "been sent to %(email)s." | |
125 | msgstr "%(within)s以内にパスワードを設定しませんでした。パスワード再設定手順を %(email)s に再度送信しました。" | |
126 | ||
127 | #: flask_security/core.py:317 | |
128 | msgid "Invalid reset password token." | |
129 | msgstr "リンクが無効です" | |
130 | ||
131 | #: flask_security/core.py:318 | |
132 | msgid "Email requires confirmation." | |
133 | msgstr "メール アドレスの検証が必要です" | |
134 | ||
135 | #: flask_security/core.py:320 | |
136 | #, python-format | |
137 | msgid "Confirmation instructions have been sent to %(email)s." | |
138 | msgstr "%(email)sにメール アドレス検証手順が再送信されました" | |
139 | ||
140 | #: flask_security/core.py:324 | |
141 | #, python-format | |
142 | msgid "" | |
143 | "You did not confirm your email within %(within)s. New instructions to " | |
144 | "confirm your email have been sent to %(email)s." | |
145 | msgstr "%(within)s以内にメール アドレスが検証されませんでした。新しい検証手順を %(email)s に送信しました。" | |
146 | ||
147 | #: flask_security/core.py:332 | |
148 | #, python-format | |
149 | msgid "" | |
150 | "You did not login within %(within)s. New instructions to login have been " | |
151 | "sent to %(email)s." | |
152 | msgstr "%(within)s以内にログインしませんでした。ログイン手順を %(email)s に再度送信しました。" | |
153 | ||
154 | #: flask_security/core.py:339 | |
155 | #, python-format | |
156 | msgid "Instructions to login have been sent to %(email)s." | |
157 | msgstr "%(email)sにログイン手順が送信されました" | |
158 | ||
159 | #: flask_security/core.py:342 | |
160 | msgid "Invalid login token." | |
161 | msgstr "リンクが無効です" | |
162 | ||
163 | #: flask_security/core.py:343 | |
164 | msgid "Account is disabled." | |
165 | msgstr "アカウントが無効になっています" | |
166 | ||
167 | #: flask_security/core.py:344 | |
168 | msgid "Email not provided" | |
169 | msgstr "メール アドレスを入力してください" | |
170 | ||
171 | #: flask_security/core.py:345 | |
172 | msgid "Invalid email address" | |
173 | msgstr "正しいメール アドレスを入力してください" | |
174 | ||
175 | #: flask_security/core.py:346 | |
176 | #, fuzzy | |
177 | msgid "Invalid code" | |
178 | msgstr "入力を確認してください" | |
179 | ||
180 | #: flask_security/core.py:347 | |
181 | msgid "Password not provided" | |
182 | msgstr "パスワードを入力してください" | |
183 | ||
184 | #: flask_security/core.py:348 | |
185 | msgid "No password is set for this user" | |
186 | msgstr "パスワードが設定されていません" | |
187 | ||
188 | #: flask_security/core.py:350 | |
189 | #, fuzzy, python-format | |
190 | msgid "Password must be at least %(length)s characters" | |
191 | msgstr "パスワードは6文字以上でなければなりません" | |
192 | ||
193 | #: flask_security/core.py:353 | |
194 | msgid "Password not complex enough" | |
195 | msgstr "" | |
196 | ||
197 | #: flask_security/core.py:354 | |
198 | msgid "Password on breached list" | |
199 | msgstr "" | |
200 | ||
201 | #: flask_security/core.py:356 | |
202 | msgid "Failed to contact breached passwords site" | |
203 | msgstr "" | |
204 | ||
205 | #: flask_security/core.py:359 | |
206 | msgid "Phone number not valid e.g. missing country code" | |
207 | msgstr "" | |
208 | ||
209 | #: flask_security/core.py:360 | |
210 | msgid "Specified user does not exist" | |
211 | msgstr "入力を確認してください" | |
212 | ||
213 | #: flask_security/core.py:361 | |
214 | msgid "Invalid password" | |
215 | msgstr "入力を確認してください" | |
216 | ||
217 | #: flask_security/core.py:362 | |
218 | msgid "Password or code submitted is not valid" | |
219 | msgstr "" | |
220 | ||
221 | #: flask_security/core.py:363 | |
222 | msgid "You have successfully logged in." | |
223 | msgstr "ログインしました" | |
224 | ||
225 | #: flask_security/core.py:364 | |
226 | msgid "Forgot password?" | |
227 | msgstr "パスワードを忘れた場合" | |
228 | ||
229 | #: flask_security/core.py:366 | |
230 | msgid "" | |
231 | "You successfully reset your password and you have been logged in " | |
232 | "automatically." | |
233 | msgstr "パスワードの再設定が完了しました。" | |
234 | ||
235 | #: flask_security/core.py:373 | |
236 | msgid "Your new password must be different than your previous password." | |
237 | msgstr "新旧パスワードが同じです" | |
238 | ||
239 | #: flask_security/core.py:376 | |
240 | msgid "You successfully changed your password." | |
241 | msgstr "パスワードが変更されました" | |
242 | ||
243 | #: flask_security/core.py:377 | |
244 | msgid "Please log in to access this page." | |
245 | msgstr "ログインしてください" | |
246 | ||
247 | #: flask_security/core.py:378 | |
248 | msgid "Please reauthenticate to access this page." | |
249 | msgstr "再度ログインしてください" | |
250 | ||
251 | #: flask_security/core.py:379 | |
252 | msgid "Reauthentication successful" | |
253 | msgstr "" | |
254 | ||
255 | #: flask_security/core.py:381 | |
256 | msgid "You can only access this endpoint when not logged in." | |
257 | msgstr "" | |
258 | ||
259 | #: flask_security/core.py:384 | |
260 | msgid "Failed to send code. Please try again later" | |
261 | msgstr "" | |
262 | ||
263 | #: flask_security/core.py:385 | |
264 | msgid "Invalid Token" | |
265 | msgstr "" | |
266 | ||
267 | #: flask_security/core.py:386 | |
268 | msgid "Your token has been confirmed" | |
269 | msgstr "" | |
270 | ||
271 | #: flask_security/core.py:388 | |
272 | msgid "You successfully changed your two-factor method." | |
273 | msgstr "" | |
274 | ||
275 | #: flask_security/core.py:392 | |
276 | msgid "You successfully confirmed password" | |
277 | msgstr "" | |
278 | ||
279 | #: flask_security/core.py:396 | |
280 | msgid "Password confirmation is needed in order to access page" | |
281 | msgstr "" | |
282 | ||
283 | #: flask_security/core.py:400 | |
284 | msgid "You currently do not have permissions to access this page" | |
285 | msgstr "" | |
286 | ||
287 | #: flask_security/core.py:403 | |
288 | msgid "Marked method is not valid" | |
289 | msgstr "" | |
290 | ||
291 | #: flask_security/core.py:405 | |
292 | msgid "You successfully disabled two factor authorization." | |
293 | msgstr "" | |
294 | ||
295 | #: flask_security/core.py:408 | |
296 | msgid "Requested method is not valid" | |
297 | msgstr "" | |
298 | ||
299 | #: flask_security/core.py:410 | |
300 | #, python-format | |
301 | msgid "Setup must be completed within %(within)s. Please start over." | |
302 | msgstr "" | |
303 | ||
304 | #: flask_security/core.py:413 | |
305 | msgid "Unified sign in setup successful" | |
306 | msgstr "" | |
307 | ||
308 | #: flask_security/core.py:414 | |
309 | msgid "You must specify a valid identity to sign in" | |
310 | msgstr "" | |
311 | ||
312 | #: flask_security/core.py:415 | |
313 | #, python-format | |
314 | msgid "Use this code to sign in: %(code)s." | |
315 | msgstr "" | |
316 | ||
317 | #: flask_security/forms.py:50 | |
318 | msgid "Email Address" | |
319 | msgstr "メール アドレス" | |
320 | ||
321 | #: flask_security/forms.py:51 | |
322 | msgid "Password" | |
323 | msgstr "パスワード" | |
324 | ||
325 | #: flask_security/forms.py:52 | |
326 | msgid "Remember Me" | |
327 | msgstr "次回以降ログインを省略する" | |
328 | ||
329 | #: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 | |
330 | #: flask_security/templates/security/login_user.html:6 | |
331 | #: flask_security/templates/security/send_login.html:6 | |
332 | msgid "Login" | |
333 | msgstr "ログイン" | |
334 | ||
335 | #: flask_security/forms.py:54 | |
336 | #: flask_security/templates/security/email/us_instructions.html:8 | |
337 | #: flask_security/templates/security/us_signin.html:6 | |
338 | msgid "Sign In" | |
339 | msgstr "" | |
340 | ||
341 | #: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11 | |
342 | #: flask_security/templates/security/register_user.html:6 | |
343 | msgid "Register" | |
344 | msgstr "ユーザ登録" | |
345 | ||
346 | #: flask_security/forms.py:56 | |
347 | msgid "Resend Confirmation Instructions" | |
348 | msgstr "検証手順の再送信" | |
349 | ||
350 | #: flask_security/forms.py:57 | |
351 | msgid "Recover Password" | |
352 | msgstr "再設定手順を送信" | |
353 | ||
354 | #: flask_security/forms.py:58 | |
355 | msgid "Reset Password" | |
356 | msgstr "パスワード変更" | |
357 | ||
358 | #: flask_security/forms.py:59 | |
359 | msgid "Retype Password" | |
360 | msgstr "パスワード再入力" | |
361 | ||
362 | #: flask_security/forms.py:60 | |
363 | msgid "New Password" | |
364 | msgstr "新しいパスワード" | |
365 | ||
366 | #: flask_security/forms.py:61 | |
367 | msgid "Change Password" | |
368 | msgstr "変更" | |
369 | ||
370 | #: flask_security/forms.py:62 | |
371 | msgid "Send Login Link" | |
372 | msgstr "ログイン手順を送信" | |
373 | ||
374 | #: flask_security/forms.py:63 | |
375 | msgid "Verify Password" | |
376 | msgstr "" | |
377 | ||
378 | #: flask_security/forms.py:64 | |
379 | msgid "Change Method" | |
380 | msgstr "" | |
381 | ||
382 | #: flask_security/forms.py:65 | |
383 | msgid "Phone Number" | |
384 | msgstr "" | |
385 | ||
386 | #: flask_security/forms.py:66 | |
387 | msgid "Authentication Code" | |
388 | msgstr "" | |
389 | ||
390 | #: flask_security/forms.py:67 | |
391 | msgid "Submit" | |
392 | msgstr "" | |
393 | ||
394 | #: flask_security/forms.py:68 | |
395 | msgid "Submit Code" | |
396 | msgstr "" | |
397 | ||
398 | #: flask_security/forms.py:69 | |
399 | msgid "Error(s)" | |
400 | msgstr "" | |
401 | ||
402 | #: flask_security/forms.py:70 | |
403 | msgid "Identity" | |
404 | msgstr "" | |
405 | ||
406 | #: flask_security/forms.py:71 | |
407 | msgid "Send Code" | |
408 | msgstr "" | |
409 | ||
410 | #: flask_security/forms.py:72 | |
411 | #, fuzzy | |
412 | msgid "Passcode" | |
413 | msgstr "パスワード" | |
414 | ||
415 | #: flask_security/unified_signin.py:145 | |
416 | #, fuzzy | |
417 | msgid "Code or Password" | |
418 | msgstr "再設定手順を送信" | |
419 | ||
420 | #: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 | |
421 | msgid "Available Methods" | |
422 | msgstr "" | |
423 | ||
424 | #: flask_security/unified_signin.py:151 | |
425 | msgid "Via email" | |
426 | msgstr "" | |
427 | ||
428 | #: flask_security/unified_signin.py:151 | |
429 | msgid "Via SMS" | |
430 | msgstr "" | |
431 | ||
432 | #: flask_security/unified_signin.py:272 | |
433 | msgid "Set up using email" | |
434 | msgstr "" | |
435 | ||
436 | #: flask_security/unified_signin.py:275 | |
437 | msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" | |
438 | msgstr "" | |
439 | ||
440 | #: flask_security/unified_signin.py:277 | |
441 | msgid "Set up using SMS" | |
442 | msgstr "" | |
443 | ||
444 | #: flask_security/templates/security/_menu.html:2 | |
445 | msgid "Menu" | |
446 | msgstr "メニュー" | |
447 | ||
448 | #: flask_security/templates/security/_menu.html:8 | |
449 | msgid "Unified Sign In" | |
450 | msgstr "" | |
451 | ||
452 | #: flask_security/templates/security/_menu.html:14 | |
453 | msgid "Forgot password" | |
454 | msgstr "パスワードを忘れた場合" | |
455 | ||
456 | #: flask_security/templates/security/_menu.html:17 | |
457 | msgid "Confirm account" | |
458 | msgstr "メール アドレスの検証" | |
459 | ||
460 | #: flask_security/templates/security/change_password.html:6 | |
461 | msgid "Change password" | |
462 | msgstr "パスワードの変更" | |
463 | ||
464 | #: flask_security/templates/security/forgot_password.html:6 | |
465 | msgid "Send password reset instructions" | |
466 | msgstr "パスワード再設定手順の送信" | |
467 | ||
468 | #: flask_security/templates/security/reset_password.html:6 | |
469 | msgid "Reset password" | |
470 | msgstr "パスワード再設定" | |
471 | ||
472 | #: flask_security/templates/security/send_confirmation.html:6 | |
473 | msgid "Resend confirmation instructions" | |
474 | msgstr "検証手順の再送信" | |
475 | ||
476 | #: flask_security/templates/security/two_factor_setup.html:6 | |
477 | msgid "Two-factor authentication adds an extra layer of security to your account" | |
478 | msgstr "" | |
479 | ||
480 | #: flask_security/templates/security/two_factor_setup.html:7 | |
481 | msgid "" | |
482 | "In addition to your username and password, you'll need to use a code that" | |
483 | " we will send you" | |
484 | msgstr "" | |
485 | ||
486 | #: flask_security/templates/security/two_factor_setup.html:18 | |
487 | msgid "To complete logging in, please enter the code sent to your mail" | |
488 | msgstr "" | |
489 | ||
490 | #: flask_security/templates/security/two_factor_setup.html:21 | |
491 | #: flask_security/templates/security/us_setup.html:21 | |
492 | msgid "" | |
493 | "Open your authenticator app on your device and scan the following qrcode " | |
494 | "to start receiving codes:" | |
495 | msgstr "" | |
496 | ||
497 | #: flask_security/templates/security/two_factor_setup.html:22 | |
498 | msgid "Two factor authentication code" | |
499 | msgstr "" | |
500 | ||
501 | #: flask_security/templates/security/two_factor_setup.html:25 | |
502 | msgid "To Which Phone Number Should We Send Code To?" | |
503 | msgstr "" | |
504 | ||
505 | #: flask_security/templates/security/two_factor_verify_code.html:6 | |
506 | msgid "Two-factor Authentication" | |
507 | msgstr "" | |
508 | ||
509 | #: flask_security/templates/security/two_factor_verify_code.html:7 | |
510 | msgid "Please enter your authentication code" | |
511 | msgstr "" | |
512 | ||
513 | #: flask_security/templates/security/two_factor_verify_code.html:18 | |
514 | msgid "The code for authentication was sent to your email address" | |
515 | msgstr "" | |
516 | ||
517 | #: flask_security/templates/security/two_factor_verify_code.html:21 | |
518 | msgid "A mail was sent to us in order to reset your application account" | |
519 | msgstr "" | |
520 | ||
521 | #: flask_security/templates/security/two_factor_verify_password.html:6 | |
522 | #: flask_security/templates/security/verify.html:6 | |
523 | msgid "Please Enter Your Password" | |
524 | msgstr "" | |
525 | ||
526 | #: flask_security/templates/security/us_setup.html:6 | |
527 | msgid "Setup Unified Sign In options" | |
528 | msgstr "" | |
529 | ||
530 | #: flask_security/templates/security/us_setup.html:22 | |
531 | msgid "Passwordless QRCode" | |
532 | msgstr "" | |
533 | ||
534 | #: flask_security/templates/security/us_setup.html:25 | |
535 | #: flask_security/templates/security/us_signin.html:23 | |
536 | #: flask_security/templates/security/us_verify.html:21 | |
537 | #, fuzzy | |
538 | msgid "Code has been sent" | |
539 | msgstr "パスワード変更" | |
540 | ||
541 | #: flask_security/templates/security/us_setup.html:29 | |
542 | msgid "No methods have been enabled - nothing to setup" | |
543 | msgstr "" | |
544 | ||
545 | #: flask_security/templates/security/us_signin.html:15 | |
546 | #: flask_security/templates/security/us_verify.html:13 | |
547 | msgid "Request one-time code be sent" | |
548 | msgstr "" | |
549 | ||
550 | #: flask_security/templates/security/us_verify.html:6 | |
551 | #, fuzzy | |
552 | msgid "Please re-authenticate" | |
553 | msgstr "再度ログインしてください" | |
554 | ||
555 | #: flask_security/templates/security/email/change_notice.html:1 | |
556 | msgid "Your password has been changed." | |
557 | msgstr "パスワードが変更されました。" | |
558 | ||
559 | #: flask_security/templates/security/email/change_notice.html:3 | |
560 | msgid "If you did not change your password," | |
561 | msgstr "パスワードを変更した覚えがない場合には、" | |
562 | ||
563 | #: flask_security/templates/security/email/change_notice.html:3 | |
564 | msgid "click here to reset it" | |
565 | msgstr "このリンクを開いてください。" | |
566 | ||
567 | #: flask_security/templates/security/email/confirmation_instructions.html:1 | |
568 | msgid "Please confirm your email through the link below:" | |
569 | msgstr "以下のリンクからメール アドレスを検証してください:" | |
570 | ||
571 | #: flask_security/templates/security/email/confirmation_instructions.html:3 | |
572 | #: flask_security/templates/security/email/welcome.html:6 | |
573 | msgid "Confirm my account" | |
574 | msgstr "メール アドレスの検証" | |
575 | ||
576 | #: flask_security/templates/security/email/login_instructions.html:1 | |
577 | #: flask_security/templates/security/email/welcome.html:1 | |
578 | #, python-format | |
579 | msgid "Welcome %(email)s!" | |
580 | msgstr "ようこそ %(email)s !" | |
581 | ||
582 | #: flask_security/templates/security/email/login_instructions.html:3 | |
583 | msgid "You can log into your account through the link below:" | |
584 | msgstr "以下のリンクによりログインできます。" | |
585 | ||
586 | #: flask_security/templates/security/email/login_instructions.html:5 | |
587 | msgid "Login now" | |
588 | msgstr "ログイン" | |
589 | ||
590 | #: flask_security/templates/security/email/reset_instructions.html:1 | |
591 | msgid "Click here to reset your password" | |
592 | msgstr "パスワードを再設定するためにこのリンクを開いてください。" | |
593 | ||
594 | #: flask_security/templates/security/email/two_factor_instructions.html:3 | |
595 | msgid "You can log into your account using the following code:" | |
596 | msgstr "" | |
597 | ||
598 | #: flask_security/templates/security/email/two_factor_rescue.html:1 | |
599 | msgid "can not access mail account" | |
600 | msgstr "" | |
601 | ||
602 | #: flask_security/templates/security/email/us_instructions.html:3 | |
603 | #, fuzzy | |
604 | msgid "You can sign into your account using the following code:" | |
605 | msgstr "以下のリンクによりログインできます。" | |
606 | ||
607 | #: flask_security/templates/security/email/us_instructions.html:6 | |
608 | #, fuzzy | |
609 | msgid "Or use the the link below:" | |
610 | msgstr "以下のリンクによりメール アドレスを検証できます。" | |
611 | ||
612 | #: flask_security/templates/security/email/welcome.html:4 | |
613 | msgid "You can confirm your email through the link below:" | |
614 | msgstr "以下のリンクによりメール アドレスを検証できます。" | |
615 |
+638
-0
0 | # Dutch (Netherlands) translations for Flask-Security. | |
1 | # Copyright (C) 2017 CERN | |
2 | # This file is distributed under the same license as the Flask-Security | |
3 | # project. | |
4 | # FIRST AUTHOR <EMAIL@ADDRESS>, 2017. | |
5 | # | |
6 | msgid "" | |
7 | msgstr "" | |
8 | "Project-Id-Version: Flask-Security 2.0.1\n" | |
9 | "Report-Msgid-Bugs-To: [email protected]\n" | |
10 | "POT-Creation-Date: 2020-04-19 13:18-0700\n" | |
11 | "PO-Revision-Date: 2017-05-01 17:52+0200\n" | |
12 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | |
13 | "Language: nl_NL\n" | |
14 | "Language-Team: nl_NL <[email protected]>\n" | |
15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" | |
16 | "MIME-Version: 1.0\n" | |
17 | "Content-Type: text/plain; charset=utf-8\n" | |
18 | "Content-Transfer-Encoding: 8bit\n" | |
19 | "Generated-By: Babel 2.8.0\n" | |
20 | ||
21 | #: flask_security/core.py:207 | |
22 | msgid "Login Required" | |
23 | msgstr "Inloggen Verplicht" | |
24 | ||
25 | #: flask_security/core.py:208 | |
26 | #: flask_security/templates/security/email/two_factor_instructions.html:1 | |
27 | #: flask_security/templates/security/email/us_instructions.html:1 | |
28 | msgid "Welcome" | |
29 | msgstr "Welkom" | |
30 | ||
31 | #: flask_security/core.py:209 | |
32 | msgid "Please confirm your email" | |
33 | msgstr "Gelieve uw e-mailadres te bevestigen" | |
34 | ||
35 | #: flask_security/core.py:210 | |
36 | msgid "Login instructions" | |
37 | msgstr "Aanmeld instructies" | |
38 | ||
39 | #: flask_security/core.py:211 | |
40 | #: flask_security/templates/security/email/reset_notice.html:1 | |
41 | msgid "Your password has been reset" | |
42 | msgstr "Uw wachtwoord werd gereset" | |
43 | ||
44 | #: flask_security/core.py:212 | |
45 | msgid "Your password has been changed" | |
46 | msgstr "Uw wachtwoord werd gewijzigd" | |
47 | ||
48 | #: flask_security/core.py:213 | |
49 | msgid "Password reset instructions" | |
50 | msgstr "Wachtwoord reset instructies" | |
51 | ||
52 | #: flask_security/core.py:216 | |
53 | msgid "Two-factor Login" | |
54 | msgstr "Dubbele Authenticatie Aanmelding" | |
55 | ||
56 | #: flask_security/core.py:217 | |
57 | msgid "Two-factor Rescue" | |
58 | msgstr "Dubbele Authenticatie Herstellen" | |
59 | ||
60 | #: flask_security/core.py:266 | |
61 | #, fuzzy | |
62 | msgid "Verification Code" | |
63 | msgstr "Authenticatie Code" | |
64 | ||
65 | #: flask_security/core.py:282 | |
66 | msgid "Input not appropriate for requested API" | |
67 | msgstr "" | |
68 | ||
69 | #: flask_security/core.py:283 | |
70 | msgid "You do not have permission to view this resource." | |
71 | msgstr "U heeft niet de nodige rechten om deze pagina te zien." | |
72 | ||
73 | #: flask_security/core.py:285 | |
74 | msgid "You are not authenticated. Please supply the correct credentials." | |
75 | msgstr "U bent niet aangemeld. Voer alstublieft de juiste gegevens in." | |
76 | ||
77 | #: flask_security/core.py:289 | |
78 | #, fuzzy | |
79 | msgid "You must re-authenticate to access this endpoint" | |
80 | msgstr "Gelieve opnieuw in te loggen om deze pagina te zien." | |
81 | ||
82 | #: flask_security/core.py:293 | |
83 | #, python-format | |
84 | msgid "Thank you. Confirmation instructions have been sent to %(email)s." | |
85 | msgstr "Bedankt. Instructies voor bevestiging zijn verzonden naar %(email)s." | |
86 | ||
87 | #: flask_security/core.py:296 | |
88 | msgid "Thank you. Your email has been confirmed." | |
89 | msgstr "Bedankt. Uw e-mailadres werd bevestigd." | |
90 | ||
91 | #: flask_security/core.py:297 | |
92 | msgid "Your email has already been confirmed." | |
93 | msgstr "Uw e-mailadres werd reeds bevestigd." | |
94 | ||
95 | #: flask_security/core.py:298 | |
96 | msgid "Invalid confirmation token." | |
97 | msgstr "Ongeldige bevestiging token." | |
98 | ||
99 | #: flask_security/core.py:300 | |
100 | #, python-format | |
101 | msgid "%(email)s is already associated with an account." | |
102 | msgstr "%(email)s is al gelinkt aan een ander account." | |
103 | ||
104 | #: flask_security/core.py:303 | |
105 | msgid "Password does not match" | |
106 | msgstr "Wachtwoord komt niet overeen" | |
107 | ||
108 | #: flask_security/core.py:304 | |
109 | msgid "Passwords do not match" | |
110 | msgstr "Wachtwoorden komen niet overeen" | |
111 | ||
112 | #: flask_security/core.py:305 | |
113 | msgid "Redirections outside the domain are forbidden" | |
114 | msgstr "Omleidingen buiten het domein zijn niet toegelaten" | |
115 | ||
116 | #: flask_security/core.py:307 | |
117 | #, python-format | |
118 | msgid "Instructions to reset your password have been sent to %(email)s." | |
119 | msgstr "Instructies om uw wachtwoord te resetten werden verzonden naar %(email)s." | |
120 | ||
121 | #: flask_security/core.py:311 | |
122 | #, python-format | |
123 | msgid "" | |
124 | "You did not reset your password within %(within)s. New instructions have " | |
125 | "been sent to %(email)s." | |
126 | msgstr "" | |
127 | "U heeft uw wachtwoord niet gereset gedurende %(within)s. Nieuwe " | |
128 | "instructies werden verzonden naar %(email)s." | |
129 | ||
130 | #: flask_security/core.py:317 | |
131 | msgid "Invalid reset password token." | |
132 | msgstr "Ongeldig wachtwoord reset token." | |
133 | ||
134 | #: flask_security/core.py:318 | |
135 | msgid "Email requires confirmation." | |
136 | msgstr "E-mailadres moet bevestigd worden." | |
137 | ||
138 | #: flask_security/core.py:320 | |
139 | #, python-format | |
140 | msgid "Confirmation instructions have been sent to %(email)s." | |
141 | msgstr "" | |
142 | "Instructies ter bevestiging van uw e-mailadres werden verzonden naar " | |
143 | "%(email)s." | |
144 | ||
145 | #: flask_security/core.py:324 | |
146 | #, python-format | |
147 | msgid "" | |
148 | "You did not confirm your email within %(within)s. New instructions to " | |
149 | "confirm your email have been sent to %(email)s." | |
150 | msgstr "" | |
151 | "U heeft uw e-mailadres niet bevestigd in de voorziene %(within)s. Nieuwe " | |
152 | "instructies ter bevestiging van uw e-mailadres werden verzonden naar " | |
153 | "%(email)s." | |
154 | ||
155 | #: flask_security/core.py:332 | |
156 | #, python-format | |
157 | msgid "" | |
158 | "You did not login within %(within)s. New instructions to login have been " | |
159 | "sent to %(email)s." | |
160 | msgstr "" | |
161 | "Je bent niet ingelogd geweest gedurende %(within)s. Nieuwe instructies om" | |
162 | " in te loggen werden verzonden naar%(email)s." | |
163 | ||
164 | #: flask_security/core.py:339 | |
165 | #, python-format | |
166 | msgid "Instructions to login have been sent to %(email)s." | |
167 | msgstr "Instructies om in te loggen werden verzonden naar %(email)s." | |
168 | ||
169 | #: flask_security/core.py:342 | |
170 | msgid "Invalid login token." | |
171 | msgstr "Ongeldige aanmelding." | |
172 | ||
173 | #: flask_security/core.py:343 | |
174 | msgid "Account is disabled." | |
175 | msgstr "Account is geblokkeerd." | |
176 | ||
177 | #: flask_security/core.py:344 | |
178 | msgid "Email not provided" | |
179 | msgstr "Email niet ingevuld" | |
180 | ||
181 | #: flask_security/core.py:345 | |
182 | msgid "Invalid email address" | |
183 | msgstr "Ongeldig e-mailadres" | |
184 | ||
185 | #: flask_security/core.py:346 | |
186 | #, fuzzy | |
187 | msgid "Invalid code" | |
188 | msgstr "Niet valide token" | |
189 | ||
190 | #: flask_security/core.py:347 | |
191 | msgid "Password not provided" | |
192 | msgstr "Wachtwoord niet ingevuld" | |
193 | ||
194 | #: flask_security/core.py:348 | |
195 | msgid "No password is set for this user" | |
196 | msgstr "Er is geen wachtwoord gezet voor deze gebruiker" | |
197 | ||
198 | #: flask_security/core.py:350 | |
199 | #, fuzzy, python-format | |
200 | msgid "Password must be at least %(length)s characters" | |
201 | msgstr "Uw wachtwoord moet minstens 6 karakters bevatten" | |
202 | ||
203 | #: flask_security/core.py:353 | |
204 | msgid "Password not complex enough" | |
205 | msgstr "" | |
206 | ||
207 | #: flask_security/core.py:354 | |
208 | msgid "Password on breached list" | |
209 | msgstr "" | |
210 | ||
211 | #: flask_security/core.py:356 | |
212 | msgid "Failed to contact breached passwords site" | |
213 | msgstr "" | |
214 | ||
215 | #: flask_security/core.py:359 | |
216 | msgid "Phone number not valid e.g. missing country code" | |
217 | msgstr "" | |
218 | ||
219 | #: flask_security/core.py:360 | |
220 | msgid "Specified user does not exist" | |
221 | msgstr "Deze gebruiker bestaat niet" | |
222 | ||
223 | #: flask_security/core.py:361 | |
224 | msgid "Invalid password" | |
225 | msgstr "Ongeldig wachtwoord" | |
226 | ||
227 | #: flask_security/core.py:362 | |
228 | #, fuzzy | |
229 | msgid "Password or code submitted is not valid" | |
230 | msgstr "De gemarkeerde methode is niet valide" | |
231 | ||
232 | #: flask_security/core.py:363 | |
233 | msgid "You have successfully logged in." | |
234 | msgstr "U bent succesvol ingelogd." | |
235 | ||
236 | #: flask_security/core.py:364 | |
237 | msgid "Forgot password?" | |
238 | msgstr "Wachtwoord vergeten?" | |
239 | ||
240 | #: flask_security/core.py:366 | |
241 | msgid "" | |
242 | "You successfully reset your password and you have been logged in " | |
243 | "automatically." | |
244 | msgstr "U heeft uw wachtwoord succesvol gereset en bent nu automatisch ingelogd." | |
245 | ||
246 | #: flask_security/core.py:373 | |
247 | msgid "Your new password must be different than your previous password." | |
248 | msgstr "Uw nieuw wachtwoord moet verschillend zijn van het voorgaande wachtwoord." | |
249 | ||
250 | #: flask_security/core.py:376 | |
251 | msgid "You successfully changed your password." | |
252 | msgstr "Uw wachtwoord werd met succes gewijzigd." | |
253 | ||
254 | #: flask_security/core.py:377 | |
255 | msgid "Please log in to access this page." | |
256 | msgstr "Gelieve in te loggen om deze pagina te zien." | |
257 | ||
258 | #: flask_security/core.py:378 | |
259 | msgid "Please reauthenticate to access this page." | |
260 | msgstr "Gelieve opnieuw in te loggen om deze pagina te zien." | |
261 | ||
262 | #: flask_security/core.py:379 | |
263 | #, fuzzy | |
264 | msgid "Reauthentication successful" | |
265 | msgstr "Authenticatie Code" | |
266 | ||
267 | #: flask_security/core.py:381 | |
268 | msgid "You can only access this endpoint when not logged in." | |
269 | msgstr "" | |
270 | ||
271 | #: flask_security/core.py:384 | |
272 | msgid "Failed to send code. Please try again later" | |
273 | msgstr "" | |
274 | ||
275 | #: flask_security/core.py:385 | |
276 | msgid "Invalid Token" | |
277 | msgstr "Niet valide token" | |
278 | ||
279 | #: flask_security/core.py:386 | |
280 | msgid "Your token has been confirmed" | |
281 | msgstr "Uw token is bevestigd" | |
282 | ||
283 | #: flask_security/core.py:388 | |
284 | msgid "You successfully changed your two-factor method." | |
285 | msgstr "U heeft succesvol uw Dubbele Authenticatie methode veranderd." | |
286 | ||
287 | #: flask_security/core.py:392 | |
288 | msgid "You successfully confirmed password" | |
289 | msgstr "U heeft succesvol uw wachtwoord aangepast" | |
290 | ||
291 | #: flask_security/core.py:396 | |
292 | msgid "Password confirmation is needed in order to access page" | |
293 | msgstr "Wachtwoord bevestiging is nodig voor we deze pagina kunnen laten zien" | |
294 | ||
295 | #: flask_security/core.py:400 | |
296 | msgid "You currently do not have permissions to access this page" | |
297 | msgstr "U heeft niet de juiste permissies om deze pagina te laden" | |
298 | ||
299 | #: flask_security/core.py:403 | |
300 | msgid "Marked method is not valid" | |
301 | msgstr "De gemarkeerde methode is niet valide" | |
302 | ||
303 | #: flask_security/core.py:405 | |
304 | msgid "You successfully disabled two factor authorization." | |
305 | msgstr "U heeft succesvol Dubbele Authenticatie uitgeschakeld." | |
306 | ||
307 | #: flask_security/core.py:408 | |
308 | #, fuzzy | |
309 | msgid "Requested method is not valid" | |
310 | msgstr "De gemarkeerde methode is niet valide" | |
311 | ||
312 | #: flask_security/core.py:410 | |
313 | #, python-format | |
314 | msgid "Setup must be completed within %(within)s. Please start over." | |
315 | msgstr "" | |
316 | ||
317 | #: flask_security/core.py:413 | |
318 | msgid "Unified sign in setup successful" | |
319 | msgstr "" | |
320 | ||
321 | #: flask_security/core.py:414 | |
322 | msgid "You must specify a valid identity to sign in" | |
323 | msgstr "" | |
324 | ||
325 | #: flask_security/core.py:415 | |
326 | #, python-format | |
327 | msgid "Use this code to sign in: %(code)s." | |
328 | msgstr "" | |
329 | ||
330 | #: flask_security/forms.py:50 | |
331 | msgid "Email Address" | |
332 | msgstr "E-mailadres" | |
333 | ||
334 | #: flask_security/forms.py:51 | |
335 | msgid "Password" | |
336 | msgstr "wachtwoord" | |
337 | ||
338 | #: flask_security/forms.py:52 | |
339 | msgid "Remember Me" | |
340 | msgstr "Ingelogd blijven" | |
341 | ||
342 | #: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 | |
343 | #: flask_security/templates/security/login_user.html:6 | |
344 | #: flask_security/templates/security/send_login.html:6 | |
345 | msgid "Login" | |
346 | msgstr "Aanmelden" | |
347 | ||
348 | #: flask_security/forms.py:54 | |
349 | #: flask_security/templates/security/email/us_instructions.html:8 | |
350 | #: flask_security/templates/security/us_signin.html:6 | |
351 | msgid "Sign In" | |
352 | msgstr "" | |
353 | ||
354 | #: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11 | |
355 | #: flask_security/templates/security/register_user.html:6 | |
356 | msgid "Register" | |
357 | msgstr "Registreer" | |
358 | ||
359 | #: flask_security/forms.py:56 | |
360 | msgid "Resend Confirmation Instructions" | |
361 | msgstr "Verzend instructies om te bevestigen opnieuw" | |
362 | ||
363 | #: flask_security/forms.py:57 | |
364 | msgid "Recover Password" | |
365 | msgstr "Herstel wachtwoord" | |
366 | ||
367 | #: flask_security/forms.py:58 | |
368 | msgid "Reset Password" | |
369 | msgstr "reset wachtwoord" | |
370 | ||
371 | #: flask_security/forms.py:59 | |
372 | msgid "Retype Password" | |
373 | msgstr "Type wachtwoord opnieuw" | |
374 | ||
375 | #: flask_security/forms.py:60 | |
376 | msgid "New Password" | |
377 | msgstr "Nieuw wachtwoord" | |
378 | ||
379 | #: flask_security/forms.py:61 | |
380 | msgid "Change Password" | |
381 | msgstr "Verander wachtwoord" | |
382 | ||
383 | #: flask_security/forms.py:62 | |
384 | msgid "Send Login Link" | |
385 | msgstr "Verzend aanmeld link" | |
386 | ||
387 | #: flask_security/forms.py:63 | |
388 | msgid "Verify Password" | |
389 | msgstr "Wachtwoord Verificatie" | |
390 | ||
391 | #: flask_security/forms.py:64 | |
392 | msgid "Change Method" | |
393 | msgstr "Verander Methode" | |
394 | ||
395 | #: flask_security/forms.py:65 | |
396 | msgid "Phone Number" | |
397 | msgstr "Telefoonnummer" | |
398 | ||
399 | #: flask_security/forms.py:66 | |
400 | msgid "Authentication Code" | |
401 | msgstr "Authenticatie Code" | |
402 | ||
403 | #: flask_security/forms.py:67 | |
404 | msgid "Submit" | |
405 | msgstr "" | |
406 | ||
407 | #: flask_security/forms.py:68 | |
408 | msgid "Submit Code" | |
409 | msgstr "" | |
410 | ||
411 | #: flask_security/forms.py:69 | |
412 | msgid "Error(s)" | |
413 | msgstr "" | |
414 | ||
415 | #: flask_security/forms.py:70 | |
416 | msgid "Identity" | |
417 | msgstr "" | |
418 | ||
419 | #: flask_security/forms.py:71 | |
420 | msgid "Send Code" | |
421 | msgstr "" | |
422 | ||
423 | #: flask_security/forms.py:72 | |
424 | #, fuzzy | |
425 | msgid "Passcode" | |
426 | msgstr "wachtwoord" | |
427 | ||
428 | #: flask_security/unified_signin.py:145 | |
429 | #, fuzzy | |
430 | msgid "Code or Password" | |
431 | msgstr "Herstel wachtwoord" | |
432 | ||
433 | #: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 | |
434 | msgid "Available Methods" | |
435 | msgstr "" | |
436 | ||
437 | #: flask_security/unified_signin.py:151 | |
438 | msgid "Via email" | |
439 | msgstr "" | |
440 | ||
441 | #: flask_security/unified_signin.py:151 | |
442 | msgid "Via SMS" | |
443 | msgstr "" | |
444 | ||
445 | #: flask_security/unified_signin.py:272 | |
446 | msgid "Set up using email" | |
447 | msgstr "" | |
448 | ||
449 | #: flask_security/unified_signin.py:275 | |
450 | msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" | |
451 | msgstr "" | |
452 | ||
453 | #: flask_security/unified_signin.py:277 | |
454 | msgid "Set up using SMS" | |
455 | msgstr "" | |
456 | ||
457 | #: flask_security/templates/security/_menu.html:2 | |
458 | msgid "Menu" | |
459 | msgstr "Menu" | |
460 | ||
461 | #: flask_security/templates/security/_menu.html:8 | |
462 | msgid "Unified Sign In" | |
463 | msgstr "" | |
464 | ||
465 | #: flask_security/templates/security/_menu.html:14 | |
466 | msgid "Forgot password" | |
467 | msgstr "Wachtwoord vergeten" | |
468 | ||
469 | #: flask_security/templates/security/_menu.html:17 | |
470 | msgid "Confirm account" | |
471 | msgstr "Bevestig account" | |
472 | ||
473 | #: flask_security/templates/security/change_password.html:6 | |
474 | msgid "Change password" | |
475 | msgstr "Verander wachtwoord" | |
476 | ||
477 | #: flask_security/templates/security/forgot_password.html:6 | |
478 | msgid "Send password reset instructions" | |
479 | msgstr "Verzend wachtwoord reset instructies" | |
480 | ||
481 | #: flask_security/templates/security/reset_password.html:6 | |
482 | msgid "Reset password" | |
483 | msgstr "Reset wachtwoord" | |
484 | ||
485 | #: flask_security/templates/security/send_confirmation.html:6 | |
486 | msgid "Resend confirmation instructions" | |
487 | msgstr "Verzend bevestiging instructies opnieuw" | |
488 | ||
489 | #: flask_security/templates/security/two_factor_setup.html:6 | |
490 | msgid "Two-factor authentication adds an extra layer of security to your account" | |
491 | msgstr "" | |
492 | "Dubbele Authenticatie voegt een extra laag van beveiliging toe aan uw " | |
493 | "account" | |
494 | ||
495 | #: flask_security/templates/security/two_factor_setup.html:7 | |
496 | msgid "" | |
497 | "In addition to your username and password, you'll need to use a code that" | |
498 | " we will send you" | |
499 | msgstr "" | |
500 | "Naast uw gebruikersnaam en wachtwoord, heeft u ook een code nodig dat we " | |
501 | "u zullen toezenden" | |
502 | ||
503 | #: flask_security/templates/security/two_factor_setup.html:18 | |
504 | msgid "To complete logging in, please enter the code sent to your mail" | |
505 | msgstr "" | |
506 | "Om verder in te loggen moet U de code die we naar uw e-mail hebben " | |
507 | "gezonden invoeren" | |
508 | ||
509 | #: flask_security/templates/security/two_factor_setup.html:21 | |
510 | #: flask_security/templates/security/us_setup.html:21 | |
511 | #, fuzzy | |
512 | msgid "" | |
513 | "Open your authenticator app on your device and scan the following qrcode " | |
514 | "to start receiving codes:" | |
515 | msgstr "" | |
516 | "Open Google Authenticator op uw toestel en scan de volgende qrcode om " | |
517 | "codes te kunnen ontvangen:" | |
518 | ||
519 | #: flask_security/templates/security/two_factor_setup.html:22 | |
520 | msgid "Two factor authentication code" | |
521 | msgstr "Dubbele Authenticatie code" | |
522 | ||
523 | #: flask_security/templates/security/two_factor_setup.html:25 | |
524 | msgid "To Which Phone Number Should We Send Code To?" | |
525 | msgstr "Naar welk telefoonnummer kunnen we code verzenden?" | |
526 | ||
527 | #: flask_security/templates/security/two_factor_verify_code.html:6 | |
528 | msgid "Two-factor Authentication" | |
529 | msgstr "Dubbele Authenticatie" | |
530 | ||
531 | #: flask_security/templates/security/two_factor_verify_code.html:7 | |
532 | msgid "Please enter your authentication code" | |
533 | msgstr "Voer uw authenticatie code in" | |
534 | ||
535 | #: flask_security/templates/security/two_factor_verify_code.html:18 | |
536 | msgid "The code for authentication was sent to your email address" | |
537 | msgstr "The code voor authenticatie is naar uw e-mail adres verzonden" | |
538 | ||
539 | #: flask_security/templates/security/two_factor_verify_code.html:21 | |
540 | msgid "A mail was sent to us in order to reset your application account" | |
541 | msgstr "Een bericht is naar uw e-mail adres verzonden om uw account te herstellen" | |
542 | ||
543 | #: flask_security/templates/security/two_factor_verify_password.html:6 | |
544 | #: flask_security/templates/security/verify.html:6 | |
545 | msgid "Please Enter Your Password" | |
546 | msgstr "Voer uw wachtwoord in" | |
547 | ||
548 | #: flask_security/templates/security/us_setup.html:6 | |
549 | msgid "Setup Unified Sign In options" | |
550 | msgstr "" | |
551 | ||
552 | #: flask_security/templates/security/us_setup.html:22 | |
553 | msgid "Passwordless QRCode" | |
554 | msgstr "" | |
555 | ||
556 | #: flask_security/templates/security/us_setup.html:25 | |
557 | #: flask_security/templates/security/us_signin.html:23 | |
558 | #: flask_security/templates/security/us_verify.html:21 | |
559 | #, fuzzy | |
560 | msgid "Code has been sent" | |
561 | msgstr "Uw wachtwoord werd gereset" | |
562 | ||
563 | #: flask_security/templates/security/us_setup.html:29 | |
564 | msgid "No methods have been enabled - nothing to setup" | |
565 | msgstr "" | |
566 | ||
567 | #: flask_security/templates/security/us_signin.html:15 | |
568 | #: flask_security/templates/security/us_verify.html:13 | |
569 | msgid "Request one-time code be sent" | |
570 | msgstr "" | |
571 | ||
572 | #: flask_security/templates/security/us_verify.html:6 | |
573 | #, fuzzy | |
574 | msgid "Please re-authenticate" | |
575 | msgstr "Voer uw authenticatie code in" | |
576 | ||
577 | #: flask_security/templates/security/email/change_notice.html:1 | |
578 | msgid "Your password has been changed." | |
579 | msgstr "Uw wachtwoord werd gewijzigd." | |
580 | ||
581 | #: flask_security/templates/security/email/change_notice.html:3 | |
582 | msgid "If you did not change your password," | |
583 | msgstr "Als u uw wachtwoord niet hebt aangepast," | |
584 | ||
585 | #: flask_security/templates/security/email/change_notice.html:3 | |
586 | msgid "click here to reset it" | |
587 | msgstr "Klik hier om het te resetten" | |
588 | ||
589 | #: flask_security/templates/security/email/confirmation_instructions.html:1 | |
590 | msgid "Please confirm your email through the link below:" | |
591 | msgstr "Gelieve uw e-mailadres te bevestigen via onderstaande link:" | |
592 | ||
593 | #: flask_security/templates/security/email/confirmation_instructions.html:3 | |
594 | #: flask_security/templates/security/email/welcome.html:6 | |
595 | msgid "Confirm my account" | |
596 | msgstr "Bevestig mijn account" | |
597 | ||
598 | #: flask_security/templates/security/email/login_instructions.html:1 | |
599 | #: flask_security/templates/security/email/welcome.html:1 | |
600 | #, python-format | |
601 | msgid "Welcome %(email)s!" | |
602 | msgstr "Welkom, %(email)s!" | |
603 | ||
604 | #: flask_security/templates/security/email/login_instructions.html:3 | |
605 | msgid "You can log into your account through the link below:" | |
606 | msgstr "U kunt inloggen door onderstaande link te gebruiken:" | |
607 | ||
608 | #: flask_security/templates/security/email/login_instructions.html:5 | |
609 | msgid "Login now" | |
610 | msgstr "Nu inloggen" | |
611 | ||
612 | #: flask_security/templates/security/email/reset_instructions.html:1 | |
613 | msgid "Click here to reset your password" | |
614 | msgstr "Klik hier om uw wachtwoord te resetten" | |
615 | ||
616 | #: flask_security/templates/security/email/two_factor_instructions.html:3 | |
617 | msgid "You can log into your account using the following code:" | |
618 | msgstr "U kunt inloggen door de volgende code te gebruiken:" | |
619 | ||
620 | #: flask_security/templates/security/email/two_factor_rescue.html:1 | |
621 | msgid "can not access mail account" | |
622 | msgstr "kan niet in het e-mail account" | |
623 | ||
624 | #: flask_security/templates/security/email/us_instructions.html:3 | |
625 | #, fuzzy | |
626 | msgid "You can sign into your account using the following code:" | |
627 | msgstr "U kunt inloggen door de volgende code te gebruiken:" | |
628 | ||
629 | #: flask_security/templates/security/email/us_instructions.html:6 | |
630 | #, fuzzy | |
631 | msgid "Or use the the link below:" | |
632 | msgstr "U kan uw e-mailadres bevestigen via de onderstaande link:" | |
633 | ||
634 | #: flask_security/templates/security/email/welcome.html:4 | |
635 | msgid "You can confirm your email through the link below:" | |
636 | msgstr "U kan uw e-mailadres bevestigen via de onderstaande link:" | |
637 |
+622
-0
0 | # Portuguese (Brazil) translations for Flask-Security. | |
1 | # Copyright (C) 2017 CERN | |
2 | # This file is distributed under the same license as the Flask-Security | |
3 | # project. | |
4 | # José Neto <[email protected]>, 2017. | |
5 | # | |
6 | msgid "" | |
7 | msgstr "" | |
8 | "Project-Id-Version: Flask-Security 2.0.1\n" | |
9 | "Report-Msgid-Bugs-To: [email protected]\n" | |
10 | "POT-Creation-Date: 2020-04-19 13:18-0700\n" | |
11 | "PO-Revision-Date: 2017-09-27 23:39-0300\n" | |
12 | "Last-Translator: José Neto <[email protected]>\n" | |
13 | "Language: pt_BR\n" | |
14 | "Language-Team: \n" | |
15 | "Plural-Forms: nplurals=2; plural=(n > 1)\n" | |
16 | "MIME-Version: 1.0\n" | |
17 | "Content-Type: text/plain; charset=utf-8\n" | |
18 | "Content-Transfer-Encoding: 8bit\n" | |
19 | "Generated-By: Babel 2.8.0\n" | |
20 | ||
21 | #: flask_security/core.py:207 | |
22 | msgid "Login Required" | |
23 | msgstr "Login obrigatório" | |
24 | ||
25 | #: flask_security/core.py:208 | |
26 | #: flask_security/templates/security/email/two_factor_instructions.html:1 | |
27 | #: flask_security/templates/security/email/us_instructions.html:1 | |
28 | msgid "Welcome" | |
29 | msgstr "Bem-vindo" | |
30 | ||
31 | #: flask_security/core.py:209 | |
32 | msgid "Please confirm your email" | |
33 | msgstr "Por favor, confirme seu email" | |
34 | ||
35 | #: flask_security/core.py:210 | |
36 | msgid "Login instructions" | |
37 | msgstr "Instruções de login" | |
38 | ||
39 | #: flask_security/core.py:211 | |
40 | #: flask_security/templates/security/email/reset_notice.html:1 | |
41 | msgid "Your password has been reset" | |
42 | msgstr "Sua senha foi redefinida" | |
43 | ||
44 | #: flask_security/core.py:212 | |
45 | msgid "Your password has been changed" | |
46 | msgstr "Sua senha foi alterada" | |
47 | ||
48 | #: flask_security/core.py:213 | |
49 | msgid "Password reset instructions" | |
50 | msgstr "Instruções para redfinir a senha" | |
51 | ||
52 | #: flask_security/core.py:216 | |
53 | msgid "Two-factor Login" | |
54 | msgstr "" | |
55 | ||
56 | #: flask_security/core.py:217 | |
57 | msgid "Two-factor Rescue" | |
58 | msgstr "" | |
59 | ||
60 | #: flask_security/core.py:266 | |
61 | msgid "Verification Code" | |
62 | msgstr "" | |
63 | ||
64 | #: flask_security/core.py:282 | |
65 | msgid "Input not appropriate for requested API" | |
66 | msgstr "" | |
67 | ||
68 | #: flask_security/core.py:283 | |
69 | msgid "You do not have permission to view this resource." | |
70 | msgstr "Você não tem permissão para ver este recurso" | |
71 | ||
72 | #: flask_security/core.py:285 | |
73 | msgid "You are not authenticated. Please supply the correct credentials." | |
74 | msgstr "" | |
75 | ||
76 | #: flask_security/core.py:289 | |
77 | #, fuzzy | |
78 | msgid "You must re-authenticate to access this endpoint" | |
79 | msgstr "Por favor, reautentique-se para acessar esta página." | |
80 | ||
81 | #: flask_security/core.py:293 | |
82 | #, python-format | |
83 | msgid "Thank you. Confirmation instructions have been sent to %(email)s." | |
84 | msgstr "Obrigado. As instruções para a confirmação foram enviadas para %(email)s." | |
85 | ||
86 | #: flask_security/core.py:296 | |
87 | msgid "Thank you. Your email has been confirmed." | |
88 | msgstr "Obrigado. Seu email foi confirmado." | |
89 | ||
90 | #: flask_security/core.py:297 | |
91 | msgid "Your email has already been confirmed." | |
92 | msgstr "Seu email já foi confirmado." | |
93 | ||
94 | #: flask_security/core.py:298 | |
95 | msgid "Invalid confirmation token." | |
96 | msgstr "Token de confirmação inválido." | |
97 | ||
98 | #: flask_security/core.py:300 | |
99 | #, python-format | |
100 | msgid "%(email)s is already associated with an account." | |
101 | msgstr "%(email)s já está associado a uma conta." | |
102 | ||
103 | #: flask_security/core.py:303 | |
104 | msgid "Password does not match" | |
105 | msgstr "Senha não confere" | |
106 | ||
107 | #: flask_security/core.py:304 | |
108 | msgid "Passwords do not match" | |
109 | msgstr "Senhas não conferem" | |
110 | ||
111 | #: flask_security/core.py:305 | |
112 | msgid "Redirections outside the domain are forbidden" | |
113 | msgstr "Redirecionamentos para fora do domínio são proibidos" | |
114 | ||
115 | #: flask_security/core.py:307 | |
116 | #, python-format | |
117 | msgid "Instructions to reset your password have been sent to %(email)s." | |
118 | msgstr "As instruções para redefinir sua senha foram enviadas para %(email)s." | |
119 | ||
120 | #: flask_security/core.py:311 | |
121 | #, python-format | |
122 | msgid "" | |
123 | "You did not reset your password within %(within)s. New instructions have " | |
124 | "been sent to %(email)s." | |
125 | msgstr "" | |
126 | "Você não redefiniu sua senha dentro de %(within)s. Novas instruções foram" | |
127 | " enviadas para %(email)s." | |
128 | ||
129 | #: flask_security/core.py:317 | |
130 | msgid "Invalid reset password token." | |
131 | msgstr "Token de redefinição de senha inválido." | |
132 | ||
133 | #: flask_security/core.py:318 | |
134 | msgid "Email requires confirmation." | |
135 | msgstr "O email requer confirmação." | |
136 | ||
137 | #: flask_security/core.py:320 | |
138 | #, python-format | |
139 | msgid "Confirmation instructions have been sent to %(email)s." | |
140 | msgstr "As instruções de confirmaç foram enviadas para %(email)s." | |
141 | ||
142 | #: flask_security/core.py:324 | |
143 | #, python-format | |
144 | msgid "" | |
145 | "You did not confirm your email within %(within)s. New instructions to " | |
146 | "confirm your email have been sent to %(email)s." | |
147 | msgstr "" | |
148 | "Você não confirmou seu email dentro de %(within)s. Novas instruções foram" | |
149 | " enviadas para %(email)s." | |
150 | ||
151 | #: flask_security/core.py:332 | |
152 | #, python-format | |
153 | msgid "" | |
154 | "You did not login within %(within)s. New instructions to login have been " | |
155 | "sent to %(email)s." | |
156 | msgstr "" | |
157 | "Você não logou dentro de %(within)s. Novas instruções para logar foram " | |
158 | "enviadas para %(email)s." | |
159 | ||
160 | #: flask_security/core.py:339 | |
161 | #, python-format | |
162 | msgid "Instructions to login have been sent to %(email)s." | |
163 | msgstr "Instruções para logar foram enviadas para %(email)s." | |
164 | ||
165 | #: flask_security/core.py:342 | |
166 | msgid "Invalid login token." | |
167 | msgstr "Token de login inválido." | |
168 | ||
169 | #: flask_security/core.py:343 | |
170 | msgid "Account is disabled." | |
171 | msgstr "Conta desabilitada." | |
172 | ||
173 | #: flask_security/core.py:344 | |
174 | msgid "Email not provided" | |
175 | msgstr "Email não informado" | |
176 | ||
177 | #: flask_security/core.py:345 | |
178 | msgid "Invalid email address" | |
179 | msgstr "Endereço de email inválido" | |
180 | ||
181 | #: flask_security/core.py:346 | |
182 | #, fuzzy | |
183 | msgid "Invalid code" | |
184 | msgstr "Senha inválida" | |
185 | ||
186 | #: flask_security/core.py:347 | |
187 | msgid "Password not provided" | |
188 | msgstr "Senha não informada" | |
189 | ||
190 | #: flask_security/core.py:348 | |
191 | msgid "No password is set for this user" | |
192 | msgstr "Nenhuma senha definida para este usuário" | |
193 | ||
194 | #: flask_security/core.py:350 | |
195 | #, fuzzy, python-format | |
196 | msgid "Password must be at least %(length)s characters" | |
197 | msgstr "A senha deve ter pelo menos 6 caracteres" | |
198 | ||
199 | #: flask_security/core.py:353 | |
200 | msgid "Password not complex enough" | |
201 | msgstr "" | |
202 | ||
203 | #: flask_security/core.py:354 | |
204 | msgid "Password on breached list" | |
205 | msgstr "" | |
206 | ||
207 | #: flask_security/core.py:356 | |
208 | msgid "Failed to contact breached passwords site" | |
209 | msgstr "" | |
210 | ||
211 | #: flask_security/core.py:359 | |
212 | msgid "Phone number not valid e.g. missing country code" | |
213 | msgstr "" | |
214 | ||
215 | #: flask_security/core.py:360 | |
216 | msgid "Specified user does not exist" | |
217 | msgstr "Usuário não existe" | |
218 | ||
219 | #: flask_security/core.py:361 | |
220 | msgid "Invalid password" | |
221 | msgstr "Senha inválida" | |
222 | ||
223 | #: flask_security/core.py:362 | |
224 | msgid "Password or code submitted is not valid" | |
225 | msgstr "" | |
226 | ||
227 | #: flask_security/core.py:363 | |
228 | msgid "You have successfully logged in." | |
229 | msgstr "Você logou com sucesso." | |
230 | ||
231 | #: flask_security/core.py:364 | |
232 | msgid "Forgot password?" | |
233 | msgstr "Esqueceu a senha?" | |
234 | ||
235 | #: flask_security/core.py:366 | |
236 | msgid "" | |
237 | "You successfully reset your password and you have been logged in " | |
238 | "automatically." | |
239 | msgstr "Você redefiniu sua senha com sucesso e foi logado automaticamente." | |
240 | ||
241 | #: flask_security/core.py:373 | |
242 | msgid "Your new password must be different than your previous password." | |
243 | msgstr "Sua nova senha deve ser diferente da sua senha anterior." | |
244 | ||
245 | #: flask_security/core.py:376 | |
246 | msgid "You successfully changed your password." | |
247 | msgstr "Você alterou sua senha com sucesso." | |
248 | ||
249 | #: flask_security/core.py:377 | |
250 | msgid "Please log in to access this page." | |
251 | msgstr "Por favor, logue para acessar esta página." | |
252 | ||
253 | #: flask_security/core.py:378 | |
254 | msgid "Please reauthenticate to access this page." | |
255 | msgstr "Por favor, reautentique-se para acessar esta página." | |
256 | ||
257 | #: flask_security/core.py:379 | |
258 | msgid "Reauthentication successful" | |
259 | msgstr "" | |
260 | ||
261 | #: flask_security/core.py:381 | |
262 | msgid "You can only access this endpoint when not logged in." | |
263 | msgstr "" | |
264 | ||
265 | #: flask_security/core.py:384 | |
266 | msgid "Failed to send code. Please try again later" | |
267 | msgstr "" | |
268 | ||
269 | #: flask_security/core.py:385 | |
270 | msgid "Invalid Token" | |
271 | msgstr "" | |
272 | ||
273 | #: flask_security/core.py:386 | |
274 | msgid "Your token has been confirmed" | |
275 | msgstr "" | |
276 | ||
277 | #: flask_security/core.py:388 | |
278 | msgid "You successfully changed your two-factor method." | |
279 | msgstr "" | |
280 | ||
281 | #: flask_security/core.py:392 | |
282 | msgid "You successfully confirmed password" | |
283 | msgstr "" | |
284 | ||
285 | #: flask_security/core.py:396 | |
286 | msgid "Password confirmation is needed in order to access page" | |
287 | msgstr "" | |
288 | ||
289 | #: flask_security/core.py:400 | |
290 | msgid "You currently do not have permissions to access this page" | |
291 | msgstr "" | |
292 | ||
293 | #: flask_security/core.py:403 | |
294 | msgid "Marked method is not valid" | |
295 | msgstr "" | |
296 | ||
297 | #: flask_security/core.py:405 | |
298 | msgid "You successfully disabled two factor authorization." | |
299 | msgstr "" | |
300 | ||
301 | #: flask_security/core.py:408 | |
302 | msgid "Requested method is not valid" | |
303 | msgstr "" | |
304 | ||
305 | #: flask_security/core.py:410 | |
306 | #, python-format | |
307 | msgid "Setup must be completed within %(within)s. Please start over." | |
308 | msgstr "" | |
309 | ||
310 | #: flask_security/core.py:413 | |
311 | msgid "Unified sign in setup successful" | |
312 | msgstr "" | |
313 | ||
314 | #: flask_security/core.py:414 | |
315 | msgid "You must specify a valid identity to sign in" | |
316 | msgstr "" | |
317 | ||
318 | #: flask_security/core.py:415 | |
319 | #, python-format | |
320 | msgid "Use this code to sign in: %(code)s." | |
321 | msgstr "" | |
322 | ||
323 | #: flask_security/forms.py:50 | |
324 | msgid "Email Address" | |
325 | msgstr "Endereço de email" | |
326 | ||
327 | #: flask_security/forms.py:51 | |
328 | msgid "Password" | |
329 | msgstr "Senha" | |
330 | ||
331 | #: flask_security/forms.py:52 | |
332 | msgid "Remember Me" | |
333 | msgstr "Lembre de mim" | |
334 | ||
335 | #: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 | |
336 | #: flask_security/templates/security/login_user.html:6 | |
337 | #: flask_security/templates/security/send_login.html:6 | |
338 | msgid "Login" | |
339 | msgstr "Login" | |
340 | ||
341 | #: flask_security/forms.py:54 | |
342 | #: flask_security/templates/security/email/us_instructions.html:8 | |
343 | #: flask_security/templates/security/us_signin.html:6 | |
344 | msgid "Sign In" | |
345 | msgstr "" | |
346 | ||
347 | #: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11 | |
348 | #: flask_security/templates/security/register_user.html:6 | |
349 | msgid "Register" | |
350 | msgstr "Registro" | |
351 | ||
352 | #: flask_security/forms.py:56 | |
353 | msgid "Resend Confirmation Instructions" | |
354 | msgstr "Reenviar instruções de confirmação" | |
355 | ||
356 | #: flask_security/forms.py:57 | |
357 | msgid "Recover Password" | |
358 | msgstr "Recuperar senha" | |
359 | ||
360 | #: flask_security/forms.py:58 | |
361 | msgid "Reset Password" | |
362 | msgstr "Redefinir senha" | |
363 | ||
364 | #: flask_security/forms.py:59 | |
365 | msgid "Retype Password" | |
366 | msgstr "Reescreva a senha" | |
367 | ||
368 | #: flask_security/forms.py:60 | |
369 | msgid "New Password" | |
370 | msgstr "Nova senha" | |
371 | ||
372 | #: flask_security/forms.py:61 | |
373 | msgid "Change Password" | |
374 | msgstr "Alterar senha" | |
375 | ||
376 | #: flask_security/forms.py:62 | |
377 | msgid "Send Login Link" | |
378 | msgstr "Enviar link de login" | |
379 | ||
380 | #: flask_security/forms.py:63 | |
381 | msgid "Verify Password" | |
382 | msgstr "" | |
383 | ||
384 | #: flask_security/forms.py:64 | |
385 | msgid "Change Method" | |
386 | msgstr "" | |
387 | ||
388 | #: flask_security/forms.py:65 | |
389 | msgid "Phone Number" | |
390 | msgstr "" | |
391 | ||
392 | #: flask_security/forms.py:66 | |
393 | msgid "Authentication Code" | |
394 | msgstr "" | |
395 | ||
396 | #: flask_security/forms.py:67 | |
397 | msgid "Submit" | |
398 | msgstr "" | |
399 | ||
400 | #: flask_security/forms.py:68 | |
401 | msgid "Submit Code" | |
402 | msgstr "" | |
403 | ||
404 | #: flask_security/forms.py:69 | |
405 | msgid "Error(s)" | |
406 | msgstr "" | |
407 | ||
408 | #: flask_security/forms.py:70 | |
409 | msgid "Identity" | |
410 | msgstr "" | |
411 | ||
412 | #: flask_security/forms.py:71 | |
413 | msgid "Send Code" | |
414 | msgstr "" | |
415 | ||
416 | #: flask_security/forms.py:72 | |
417 | #, fuzzy | |
418 | msgid "Passcode" | |
419 | msgstr "Senha" | |
420 | ||
421 | #: flask_security/unified_signin.py:145 | |
422 | #, fuzzy | |
423 | msgid "Code or Password" | |
424 | msgstr "Recuperar senha" | |
425 | ||
426 | #: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 | |
427 | msgid "Available Methods" | |
428 | msgstr "" | |
429 | ||
430 | #: flask_security/unified_signin.py:151 | |
431 | msgid "Via email" | |
432 | msgstr "" | |
433 | ||
434 | #: flask_security/unified_signin.py:151 | |
435 | msgid "Via SMS" | |
436 | msgstr "" | |
437 | ||
438 | #: flask_security/unified_signin.py:272 | |
439 | msgid "Set up using email" | |
440 | msgstr "" | |
441 | ||
442 | #: flask_security/unified_signin.py:275 | |
443 | msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" | |
444 | msgstr "" | |
445 | ||
446 | #: flask_security/unified_signin.py:277 | |
447 | msgid "Set up using SMS" | |
448 | msgstr "" | |
449 | ||
450 | #: flask_security/templates/security/_menu.html:2 | |
451 | msgid "Menu" | |
452 | msgstr "Menu" | |
453 | ||
454 | #: flask_security/templates/security/_menu.html:8 | |
455 | msgid "Unified Sign In" | |
456 | msgstr "" | |
457 | ||
458 | #: flask_security/templates/security/_menu.html:14 | |
459 | msgid "Forgot password" | |
460 | msgstr "Esqueceu a senha" | |
461 | ||
462 | #: flask_security/templates/security/_menu.html:17 | |
463 | msgid "Confirm account" | |
464 | msgstr "Confirmar conta" | |
465 | ||
466 | #: flask_security/templates/security/change_password.html:6 | |
467 | msgid "Change password" | |
468 | msgstr "Alterar senha" | |
469 | ||
470 | #: flask_security/templates/security/forgot_password.html:6 | |
471 | msgid "Send password reset instructions" | |
472 | msgstr "Enviar instruções para redefinir a senha" | |
473 | ||
474 | #: flask_security/templates/security/reset_password.html:6 | |
475 | msgid "Reset password" | |
476 | msgstr "Redefinir senha" | |
477 | ||
478 | #: flask_security/templates/security/send_confirmation.html:6 | |
479 | msgid "Resend confirmation instructions" | |
480 | msgstr "Reenviar instruções de confirmação" | |
481 | ||
482 | #: flask_security/templates/security/two_factor_setup.html:6 | |
483 | msgid "Two-factor authentication adds an extra layer of security to your account" | |
484 | msgstr "" | |
485 | ||
486 | #: flask_security/templates/security/two_factor_setup.html:7 | |
487 | msgid "" | |
488 | "In addition to your username and password, you'll need to use a code that" | |
489 | " we will send you" | |
490 | msgstr "" | |
491 | ||
492 | #: flask_security/templates/security/two_factor_setup.html:18 | |
493 | msgid "To complete logging in, please enter the code sent to your mail" | |
494 | msgstr "" | |
495 | ||
496 | #: flask_security/templates/security/two_factor_setup.html:21 | |
497 | #: flask_security/templates/security/us_setup.html:21 | |
498 | msgid "" | |
499 | "Open your authenticator app on your device and scan the following qrcode " | |
500 | "to start receiving codes:" | |
501 | msgstr "" | |
502 | ||
503 | #: flask_security/templates/security/two_factor_setup.html:22 | |
504 | msgid "Two factor authentication code" | |
505 | msgstr "" | |
506 | ||
507 | #: flask_security/templates/security/two_factor_setup.html:25 | |
508 | msgid "To Which Phone Number Should We Send Code To?" | |
509 | msgstr "" | |
510 | ||
511 | #: flask_security/templates/security/two_factor_verify_code.html:6 | |
512 | msgid "Two-factor Authentication" | |
513 | msgstr "" | |
514 | ||
515 | #: flask_security/templates/security/two_factor_verify_code.html:7 | |
516 | msgid "Please enter your authentication code" | |
517 | msgstr "" | |
518 | ||
519 | #: flask_security/templates/security/two_factor_verify_code.html:18 | |
520 | msgid "The code for authentication was sent to your email address" | |
521 | msgstr "" | |
522 | ||
523 | #: flask_security/templates/security/two_factor_verify_code.html:21 | |
524 | msgid "A mail was sent to us in order to reset your application account" | |
525 | msgstr "" | |
526 | ||
527 | #: flask_security/templates/security/two_factor_verify_password.html:6 | |
528 | #: flask_security/templates/security/verify.html:6 | |
529 | msgid "Please Enter Your Password" | |
530 | msgstr "" | |
531 | ||
532 | #: flask_security/templates/security/us_setup.html:6 | |
533 | msgid "Setup Unified Sign In options" | |
534 | msgstr "" | |
535 | ||
536 | #: flask_security/templates/security/us_setup.html:22 | |
537 | msgid "Passwordless QRCode" | |
538 | msgstr "" | |
539 | ||
540 | #: flask_security/templates/security/us_setup.html:25 | |
541 | #: flask_security/templates/security/us_signin.html:23 | |
542 | #: flask_security/templates/security/us_verify.html:21 | |
543 | #, fuzzy | |
544 | msgid "Code has been sent" | |
545 | msgstr "Sua senha foi redefinida" | |
546 | ||
547 | #: flask_security/templates/security/us_setup.html:29 | |
548 | msgid "No methods have been enabled - nothing to setup" | |
549 | msgstr "" | |
550 | ||
551 | #: flask_security/templates/security/us_signin.html:15 | |
552 | #: flask_security/templates/security/us_verify.html:13 | |
553 | msgid "Request one-time code be sent" | |
554 | msgstr "" | |
555 | ||
556 | #: flask_security/templates/security/us_verify.html:6 | |
557 | #, fuzzy | |
558 | msgid "Please re-authenticate" | |
559 | msgstr "Por favor, reautentique-se para acessar esta página." | |
560 | ||
561 | #: flask_security/templates/security/email/change_notice.html:1 | |
562 | msgid "Your password has been changed." | |
563 | msgstr "Sua senha foi alterada." | |
564 | ||
565 | #: flask_security/templates/security/email/change_notice.html:3 | |
566 | msgid "If you did not change your password," | |
567 | msgstr "Se você não alterou sua senha," | |
568 | ||
569 | #: flask_security/templates/security/email/change_notice.html:3 | |
570 | msgid "click here to reset it" | |
571 | msgstr "clique aqui para resetar" | |
572 | ||
573 | #: flask_security/templates/security/email/confirmation_instructions.html:1 | |
574 | msgid "Please confirm your email through the link below:" | |
575 | msgstr "Por favor, confirme seu email através do link abaixo:" | |
576 | ||
577 | #: flask_security/templates/security/email/confirmation_instructions.html:3 | |
578 | #: flask_security/templates/security/email/welcome.html:6 | |
579 | msgid "Confirm my account" | |
580 | msgstr "Confirmar minha conta" | |
581 | ||
582 | #: flask_security/templates/security/email/login_instructions.html:1 | |
583 | #: flask_security/templates/security/email/welcome.html:1 | |
584 | #, python-format | |
585 | msgid "Welcome %(email)s!" | |
586 | msgstr "Bem-vindo %(email)s!" | |
587 | ||
588 | #: flask_security/templates/security/email/login_instructions.html:3 | |
589 | msgid "You can log into your account through the link below:" | |
590 | msgstr "Você pode logar na sua conta através do link abaixo:" | |
591 | ||
592 | #: flask_security/templates/security/email/login_instructions.html:5 | |
593 | msgid "Login now" | |
594 | msgstr "Logar agora" | |
595 | ||
596 | #: flask_security/templates/security/email/reset_instructions.html:1 | |
597 | msgid "Click here to reset your password" | |
598 | msgstr "Clique aqui para redefinir sua senha" | |
599 | ||
600 | #: flask_security/templates/security/email/two_factor_instructions.html:3 | |
601 | msgid "You can log into your account using the following code:" | |
602 | msgstr "" | |
603 | ||
604 | #: flask_security/templates/security/email/two_factor_rescue.html:1 | |
605 | msgid "can not access mail account" | |
606 | msgstr "" | |
607 | ||
608 | #: flask_security/templates/security/email/us_instructions.html:3 | |
609 | #, fuzzy | |
610 | msgid "You can sign into your account using the following code:" | |
611 | msgstr "Você pode logar na sua conta através do link abaixo:" | |
612 | ||
613 | #: flask_security/templates/security/email/us_instructions.html:6 | |
614 | #, fuzzy | |
615 | msgid "Or use the the link below:" | |
616 | msgstr "Você pode confirmar seu email através do link abaixo:" | |
617 | ||
618 | #: flask_security/templates/security/email/welcome.html:4 | |
619 | msgid "You can confirm your email through the link below:" | |
620 | msgstr "Você pode confirmar seu email através do link abaixo:" | |
621 |
+626
-0
0 | # Portuguese (Portugal) translations for Flask-Security. | |
1 | # Copyright (C) 2017 CERN | |
2 | # This file is distributed under the same license as the Flask-Security | |
3 | # project. | |
4 | # Micael Grilo <[email protected]>, 2018. | |
5 | # | |
6 | msgid "" | |
7 | msgstr "" | |
8 | "Project-Id-Version: Flask-Security 2.0.1\n" | |
9 | "Report-Msgid-Bugs-To: [email protected]\n" | |
10 | "POT-Creation-Date: 2020-04-19 13:18-0700\n" | |
11 | "PO-Revision-Date: 2018-04-27 14:00+0100\n" | |
12 | "Last-Translator: Micael Grilo <[email protected]>\n" | |
13 | "Language: pt_PT\n" | |
14 | "Language-Team: \n" | |
15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" | |
16 | "MIME-Version: 1.0\n" | |
17 | "Content-Type: text/plain; charset=utf-8\n" | |
18 | "Content-Transfer-Encoding: 8bit\n" | |
19 | "Generated-By: Babel 2.8.0\n" | |
20 | ||
21 | #: flask_security/core.py:207 | |
22 | msgid "Login Required" | |
23 | msgstr "Login obrigatório" | |
24 | ||
25 | #: flask_security/core.py:208 | |
26 | #: flask_security/templates/security/email/two_factor_instructions.html:1 | |
27 | #: flask_security/templates/security/email/us_instructions.html:1 | |
28 | msgid "Welcome" | |
29 | msgstr "Bem-vindo" | |
30 | ||
31 | #: flask_security/core.py:209 | |
32 | msgid "Please confirm your email" | |
33 | msgstr "Por favor, confirme o seu email" | |
34 | ||
35 | #: flask_security/core.py:210 | |
36 | msgid "Login instructions" | |
37 | msgstr "Instruções de login" | |
38 | ||
39 | #: flask_security/core.py:211 | |
40 | #: flask_security/templates/security/email/reset_notice.html:1 | |
41 | msgid "Your password has been reset" | |
42 | msgstr "A sua palavra-passe foi redefinida" | |
43 | ||
44 | #: flask_security/core.py:212 | |
45 | msgid "Your password has been changed" | |
46 | msgstr "A sua palavra-passe foi alterada" | |
47 | ||
48 | #: flask_security/core.py:213 | |
49 | msgid "Password reset instructions" | |
50 | msgstr "Instruções para redefinir a palavra-passe" | |
51 | ||
52 | #: flask_security/core.py:216 | |
53 | msgid "Two-factor Login" | |
54 | msgstr "" | |
55 | ||
56 | #: flask_security/core.py:217 | |
57 | msgid "Two-factor Rescue" | |
58 | msgstr "" | |
59 | ||
60 | #: flask_security/core.py:266 | |
61 | msgid "Verification Code" | |
62 | msgstr "" | |
63 | ||
64 | #: flask_security/core.py:282 | |
65 | msgid "Input not appropriate for requested API" | |
66 | msgstr "" | |
67 | ||
68 | #: flask_security/core.py:283 | |
69 | msgid "You do not have permission to view this resource." | |
70 | msgstr "Não tem permissões para ver este recurso" | |
71 | ||
72 | #: flask_security/core.py:285 | |
73 | msgid "You are not authenticated. Please supply the correct credentials." | |
74 | msgstr "" | |
75 | ||
76 | #: flask_security/core.py:289 | |
77 | #, fuzzy | |
78 | msgid "You must re-authenticate to access this endpoint" | |
79 | msgstr "Por favor, reautentique-se para aceder esta página." | |
80 | ||
81 | #: flask_security/core.py:293 | |
82 | #, python-format | |
83 | msgid "Thank you. Confirmation instructions have been sent to %(email)s." | |
84 | msgstr "Obrigado. As instruções para a confirmação foram enviadas para %(email)s." | |
85 | ||
86 | #: flask_security/core.py:296 | |
87 | msgid "Thank you. Your email has been confirmed." | |
88 | msgstr "Obrigado. O seu email foi confirmado." | |
89 | ||
90 | #: flask_security/core.py:297 | |
91 | msgid "Your email has already been confirmed." | |
92 | msgstr "O seu email já foi confirmado." | |
93 | ||
94 | #: flask_security/core.py:298 | |
95 | msgid "Invalid confirmation token." | |
96 | msgstr "Token de confirmação inválido." | |
97 | ||
98 | #: flask_security/core.py:300 | |
99 | #, python-format | |
100 | msgid "%(email)s is already associated with an account." | |
101 | msgstr "%(email)s já está associado a uma conta." | |
102 | ||
103 | #: flask_security/core.py:303 | |
104 | msgid "Password does not match" | |
105 | msgstr "Palavra-passe não coincide" | |
106 | ||
107 | #: flask_security/core.py:304 | |
108 | msgid "Passwords do not match" | |
109 | msgstr "Palavras-passe não coincidem" | |
110 | ||
111 | #: flask_security/core.py:305 | |
112 | msgid "Redirections outside the domain are forbidden" | |
113 | msgstr "Redirecionamentos para fora do domínio são proibidos" | |
114 | ||
115 | #: flask_security/core.py:307 | |
116 | #, python-format | |
117 | msgid "Instructions to reset your password have been sent to %(email)s." | |
118 | msgstr "" | |
119 | "As instruções para redefinir a sua palavra-passe foram enviadas para " | |
120 | "%(email)s." | |
121 | ||
122 | #: flask_security/core.py:311 | |
123 | #, python-format | |
124 | msgid "" | |
125 | "You did not reset your password within %(within)s. New instructions have " | |
126 | "been sent to %(email)s." | |
127 | msgstr "" | |
128 | "Não redefiniu a sua palavra-passe dentro de %(within)s. Novas instruções " | |
129 | "foram enviadas para %(email)s." | |
130 | ||
131 | #: flask_security/core.py:317 | |
132 | msgid "Invalid reset password token." | |
133 | msgstr "Token de redefinição de senha inválido." | |
134 | ||
135 | #: flask_security/core.py:318 | |
136 | msgid "Email requires confirmation." | |
137 | msgstr "O email requer confirmação." | |
138 | ||
139 | #: flask_security/core.py:320 | |
140 | #, python-format | |
141 | msgid "Confirmation instructions have been sent to %(email)s." | |
142 | msgstr "As instruções de confirmação foram enviadas para %(email)s." | |
143 | ||
144 | #: flask_security/core.py:324 | |
145 | #, python-format | |
146 | msgid "" | |
147 | "You did not confirm your email within %(within)s. New instructions to " | |
148 | "confirm your email have been sent to %(email)s." | |
149 | msgstr "" | |
150 | "Não confirmou o seu email dentro de %(within)s. Novas instruções foram " | |
151 | "enviadas para %(email)s." | |
152 | ||
153 | #: flask_security/core.py:332 | |
154 | #, python-format | |
155 | msgid "" | |
156 | "You did not login within %(within)s. New instructions to login have been " | |
157 | "sent to %(email)s." | |
158 | msgstr "" | |
159 | "Não iniciou sessão dentro de %(within)s. Novas instruções de inicio de " | |
160 | "sessão foram enviadas para %(email)s." | |
161 | ||
162 | #: flask_security/core.py:339 | |
163 | #, python-format | |
164 | msgid "Instructions to login have been sent to %(email)s." | |
165 | msgstr "Instruções para o inicio de sessão foram enviadas para %(email)s." | |
166 | ||
167 | #: flask_security/core.py:342 | |
168 | msgid "Invalid login token." | |
169 | msgstr "Token de login inválido." | |
170 | ||
171 | #: flask_security/core.py:343 | |
172 | msgid "Account is disabled." | |
173 | msgstr "Conta desactivada." | |
174 | ||
175 | #: flask_security/core.py:344 | |
176 | msgid "Email not provided" | |
177 | msgstr "Email em falta" | |
178 | ||
179 | #: flask_security/core.py:345 | |
180 | msgid "Invalid email address" | |
181 | msgstr "Endereço de email inválido" | |
182 | ||
183 | #: flask_security/core.py:346 | |
184 | #, fuzzy | |
185 | msgid "Invalid code" | |
186 | msgstr "Palavra-passe inválida" | |
187 | ||
188 | #: flask_security/core.py:347 | |
189 | msgid "Password not provided" | |
190 | msgstr "Palavra-passe em falta" | |
191 | ||
192 | #: flask_security/core.py:348 | |
193 | msgid "No password is set for this user" | |
194 | msgstr "Nenhuma palavra-passe foi definida para este utilizador" | |
195 | ||
196 | #: flask_security/core.py:350 | |
197 | #, fuzzy, python-format | |
198 | msgid "Password must be at least %(length)s characters" | |
199 | msgstr "A palavra-passe deve ter pelo menos 6 caracteres" | |
200 | ||
201 | #: flask_security/core.py:353 | |
202 | msgid "Password not complex enough" | |
203 | msgstr "" | |
204 | ||
205 | #: flask_security/core.py:354 | |
206 | msgid "Password on breached list" | |
207 | msgstr "" | |
208 | ||
209 | #: flask_security/core.py:356 | |
210 | msgid "Failed to contact breached passwords site" | |
211 | msgstr "" | |
212 | ||
213 | #: flask_security/core.py:359 | |
214 | msgid "Phone number not valid e.g. missing country code" | |
215 | msgstr "" | |
216 | ||
217 | #: flask_security/core.py:360 | |
218 | msgid "Specified user does not exist" | |
219 | msgstr "Utilizador não existe" | |
220 | ||
221 | #: flask_security/core.py:361 | |
222 | msgid "Invalid password" | |
223 | msgstr "Palavra-passe inválida" | |
224 | ||
225 | #: flask_security/core.py:362 | |
226 | msgid "Password or code submitted is not valid" | |
227 | msgstr "" | |
228 | ||
229 | #: flask_security/core.py:363 | |
230 | msgid "You have successfully logged in." | |
231 | msgstr "Sessão iniciada com sucesso." | |
232 | ||
233 | #: flask_security/core.py:364 | |
234 | msgid "Forgot password?" | |
235 | msgstr "Esqueceu a palavra-passe?" | |
236 | ||
237 | #: flask_security/core.py:366 | |
238 | msgid "" | |
239 | "You successfully reset your password and you have been logged in " | |
240 | "automatically." | |
241 | msgstr "" | |
242 | "Redefiniu a sua palavra-passe com sucesso e iniciou sessão " | |
243 | "automaticamente." | |
244 | ||
245 | #: flask_security/core.py:373 | |
246 | msgid "Your new password must be different than your previous password." | |
247 | msgstr "A sua nova palavra-passe deve ser diferente da anterior." | |
248 | ||
249 | #: flask_security/core.py:376 | |
250 | msgid "You successfully changed your password." | |
251 | msgstr "Alterou a sua palavra-passe com sucesso." | |
252 | ||
253 | #: flask_security/core.py:377 | |
254 | msgid "Please log in to access this page." | |
255 | msgstr "Por favor, inicie sessão para aceder a esta página." | |
256 | ||
257 | #: flask_security/core.py:378 | |
258 | msgid "Please reauthenticate to access this page." | |
259 | msgstr "Por favor, reautentique-se para aceder esta página." | |
260 | ||
261 | #: flask_security/core.py:379 | |
262 | msgid "Reauthentication successful" | |
263 | msgstr "" | |
264 | ||
265 | #: flask_security/core.py:381 | |
266 | msgid "You can only access this endpoint when not logged in." | |
267 | msgstr "" | |
268 | ||
269 | #: flask_security/core.py:384 | |
270 | msgid "Failed to send code. Please try again later" | |
271 | msgstr "" | |
272 | ||
273 | #: flask_security/core.py:385 | |
274 | msgid "Invalid Token" | |
275 | msgstr "" | |
276 | ||
277 | #: flask_security/core.py:386 | |
278 | msgid "Your token has been confirmed" | |
279 | msgstr "" | |
280 | ||
281 | #: flask_security/core.py:388 | |
282 | msgid "You successfully changed your two-factor method." | |
283 | msgstr "" | |
284 | ||
285 | #: flask_security/core.py:392 | |
286 | msgid "You successfully confirmed password" | |
287 | msgstr "" | |
288 | ||
289 | #: flask_security/core.py:396 | |
290 | msgid "Password confirmation is needed in order to access page" | |
291 | msgstr "" | |
292 | ||
293 | #: flask_security/core.py:400 | |
294 | msgid "You currently do not have permissions to access this page" | |
295 | msgstr "" | |
296 | ||
297 | #: flask_security/core.py:403 | |
298 | msgid "Marked method is not valid" | |
299 | msgstr "" | |
300 | ||
301 | #: flask_security/core.py:405 | |
302 | msgid "You successfully disabled two factor authorization." | |
303 | msgstr "" | |
304 | ||
305 | #: flask_security/core.py:408 | |
306 | msgid "Requested method is not valid" | |
307 | msgstr "" | |
308 | ||
309 | #: flask_security/core.py:410 | |
310 | #, python-format | |
311 | msgid "Setup must be completed within %(within)s. Please start over." | |
312 | msgstr "" | |
313 | ||
314 | #: flask_security/core.py:413 | |
315 | msgid "Unified sign in setup successful" | |
316 | msgstr "" | |
317 | ||
318 | #: flask_security/core.py:414 | |
319 | msgid "You must specify a valid identity to sign in" | |
320 | msgstr "" | |
321 | ||
322 | #: flask_security/core.py:415 | |
323 | #, python-format | |
324 | msgid "Use this code to sign in: %(code)s." | |
325 | msgstr "" | |
326 | ||
327 | #: flask_security/forms.py:50 | |
328 | msgid "Email Address" | |
329 | msgstr "Endereço de email" | |
330 | ||
331 | #: flask_security/forms.py:51 | |
332 | msgid "Password" | |
333 | msgstr "Palavra-passe" | |
334 | ||
335 | #: flask_security/forms.py:52 | |
336 | msgid "Remember Me" | |
337 | msgstr "Lembrar-me" | |
338 | ||
339 | #: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 | |
340 | #: flask_security/templates/security/login_user.html:6 | |
341 | #: flask_security/templates/security/send_login.html:6 | |
342 | msgid "Login" | |
343 | msgstr "Login" | |
344 | ||
345 | #: flask_security/forms.py:54 | |
346 | #: flask_security/templates/security/email/us_instructions.html:8 | |
347 | #: flask_security/templates/security/us_signin.html:6 | |
348 | msgid "Sign In" | |
349 | msgstr "" | |
350 | ||
351 | #: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11 | |
352 | #: flask_security/templates/security/register_user.html:6 | |
353 | msgid "Register" | |
354 | msgstr "Registo" | |
355 | ||
356 | #: flask_security/forms.py:56 | |
357 | msgid "Resend Confirmation Instructions" | |
358 | msgstr "Reenviar instruções de confirmação" | |
359 | ||
360 | #: flask_security/forms.py:57 | |
361 | msgid "Recover Password" | |
362 | msgstr "Recuperar palavra-passe" | |
363 | ||
364 | #: flask_security/forms.py:58 | |
365 | msgid "Reset Password" | |
366 | msgstr "Redefinir palavra-passe" | |
367 | ||
368 | #: flask_security/forms.py:59 | |
369 | msgid "Retype Password" | |
370 | msgstr "Reescreva a palavra-passe" | |
371 | ||
372 | #: flask_security/forms.py:60 | |
373 | msgid "New Password" | |
374 | msgstr "Nova palavra-passe" | |
375 | ||
376 | #: flask_security/forms.py:61 | |
377 | msgid "Change Password" | |
378 | msgstr "Alterar palavra-passe" | |
379 | ||
380 | #: flask_security/forms.py:62 | |
381 | msgid "Send Login Link" | |
382 | msgstr "Enviar endereço de login" | |
383 | ||
384 | #: flask_security/forms.py:63 | |
385 | msgid "Verify Password" | |
386 | msgstr "" | |
387 | ||
388 | #: flask_security/forms.py:64 | |
389 | msgid "Change Method" | |
390 | msgstr "" | |
391 | ||
392 | #: flask_security/forms.py:65 | |
393 | msgid "Phone Number" | |
394 | msgstr "" | |
395 | ||
396 | #: flask_security/forms.py:66 | |
397 | msgid "Authentication Code" | |
398 | msgstr "" | |
399 | ||
400 | #: flask_security/forms.py:67 | |
401 | msgid "Submit" | |
402 | msgstr "" | |
403 | ||
404 | #: flask_security/forms.py:68 | |
405 | msgid "Submit Code" | |
406 | msgstr "" | |
407 | ||
408 | #: flask_security/forms.py:69 | |
409 | msgid "Error(s)" | |
410 | msgstr "" | |
411 | ||
412 | #: flask_security/forms.py:70 | |
413 | msgid "Identity" | |
414 | msgstr "" | |
415 | ||
416 | #: flask_security/forms.py:71 | |
417 | msgid "Send Code" | |
418 | msgstr "" | |
419 | ||
420 | #: flask_security/forms.py:72 | |
421 | #, fuzzy | |
422 | msgid "Passcode" | |
423 | msgstr "Palavra-passe" | |
424 | ||
425 | #: flask_security/unified_signin.py:145 | |
426 | #, fuzzy | |
427 | msgid "Code or Password" | |
428 | msgstr "Recuperar palavra-passe" | |
429 | ||
430 | #: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 | |
431 | msgid "Available Methods" | |
432 | msgstr "" | |
433 | ||
434 | #: flask_security/unified_signin.py:151 | |
435 | msgid "Via email" | |
436 | msgstr "" | |
437 | ||
438 | #: flask_security/unified_signin.py:151 | |
439 | msgid "Via SMS" | |
440 | msgstr "" | |
441 | ||
442 | #: flask_security/unified_signin.py:272 | |
443 | msgid "Set up using email" | |
444 | msgstr "" | |
445 | ||
446 | #: flask_security/unified_signin.py:275 | |
447 | msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" | |
448 | msgstr "" | |
449 | ||
450 | #: flask_security/unified_signin.py:277 | |
451 | msgid "Set up using SMS" | |
452 | msgstr "" | |
453 | ||
454 | #: flask_security/templates/security/_menu.html:2 | |
455 | msgid "Menu" | |
456 | msgstr "Menu" | |
457 | ||
458 | #: flask_security/templates/security/_menu.html:8 | |
459 | msgid "Unified Sign In" | |
460 | msgstr "" | |
461 | ||
462 | #: flask_security/templates/security/_menu.html:14 | |
463 | msgid "Forgot password" | |
464 | msgstr "Esqueceu a palavra-passe" | |
465 | ||
466 | #: flask_security/templates/security/_menu.html:17 | |
467 | msgid "Confirm account" | |
468 | msgstr "Confirmar conta" | |
469 | ||
470 | #: flask_security/templates/security/change_password.html:6 | |
471 | msgid "Change password" | |
472 | msgstr "Alterar palavra-passe" | |
473 | ||
474 | #: flask_security/templates/security/forgot_password.html:6 | |
475 | msgid "Send password reset instructions" | |
476 | msgstr "Enviar instruções para redefinir a palavra-passe" | |
477 | ||
478 | #: flask_security/templates/security/reset_password.html:6 | |
479 | msgid "Reset password" | |
480 | msgstr "Redefinir palavra-passe" | |
481 | ||
482 | #: flask_security/templates/security/send_confirmation.html:6 | |
483 | msgid "Resend confirmation instructions" | |
484 | msgstr "Reenviar instruções de confirmação" | |
485 | ||
486 | #: flask_security/templates/security/two_factor_setup.html:6 | |
487 | msgid "Two-factor authentication adds an extra layer of security to your account" | |
488 | msgstr "" | |
489 | ||
490 | #: flask_security/templates/security/two_factor_setup.html:7 | |
491 | msgid "" | |
492 | "In addition to your username and password, you'll need to use a code that" | |
493 | " we will send you" | |
494 | msgstr "" | |
495 | ||
496 | #: flask_security/templates/security/two_factor_setup.html:18 | |
497 | msgid "To complete logging in, please enter the code sent to your mail" | |
498 | msgstr "" | |
499 | ||
500 | #: flask_security/templates/security/two_factor_setup.html:21 | |
501 | #: flask_security/templates/security/us_setup.html:21 | |
502 | msgid "" | |
503 | "Open your authenticator app on your device and scan the following qrcode " | |
504 | "to start receiving codes:" | |
505 | msgstr "" | |
506 | ||
507 | #: flask_security/templates/security/two_factor_setup.html:22 | |
508 | msgid "Two factor authentication code" | |
509 | msgstr "" | |
510 | ||
511 | #: flask_security/templates/security/two_factor_setup.html:25 | |
512 | msgid "To Which Phone Number Should We Send Code To?" | |
513 | msgstr "" | |
514 | ||
515 | #: flask_security/templates/security/two_factor_verify_code.html:6 | |
516 | msgid "Two-factor Authentication" | |
517 | msgstr "" | |
518 | ||
519 | #: flask_security/templates/security/two_factor_verify_code.html:7 | |
520 | msgid "Please enter your authentication code" | |
521 | msgstr "" | |
522 | ||
523 | #: flask_security/templates/security/two_factor_verify_code.html:18 | |
524 | msgid "The code for authentication was sent to your email address" | |
525 | msgstr "" | |
526 | ||
527 | #: flask_security/templates/security/two_factor_verify_code.html:21 | |
528 | msgid "A mail was sent to us in order to reset your application account" | |
529 | msgstr "" | |
530 | ||
531 | #: flask_security/templates/security/two_factor_verify_password.html:6 | |
532 | #: flask_security/templates/security/verify.html:6 | |
533 | msgid "Please Enter Your Password" | |
534 | msgstr "" | |
535 | ||
536 | #: flask_security/templates/security/us_setup.html:6 | |
537 | msgid "Setup Unified Sign In options" | |
538 | msgstr "" | |
539 | ||
540 | #: flask_security/templates/security/us_setup.html:22 | |
541 | msgid "Passwordless QRCode" | |
542 | msgstr "" | |
543 | ||
544 | #: flask_security/templates/security/us_setup.html:25 | |
545 | #: flask_security/templates/security/us_signin.html:23 | |
546 | #: flask_security/templates/security/us_verify.html:21 | |
547 | #, fuzzy | |
548 | msgid "Code has been sent" | |
549 | msgstr "A sua palavra-passe foi redefinida" | |
550 | ||
551 | #: flask_security/templates/security/us_setup.html:29 | |
552 | msgid "No methods have been enabled - nothing to setup" | |
553 | msgstr "" | |
554 | ||
555 | #: flask_security/templates/security/us_signin.html:15 | |
556 | #: flask_security/templates/security/us_verify.html:13 | |
557 | msgid "Request one-time code be sent" | |
558 | msgstr "" | |
559 | ||
560 | #: flask_security/templates/security/us_verify.html:6 | |
561 | #, fuzzy | |
562 | msgid "Please re-authenticate" | |
563 | msgstr "Por favor, reautentique-se para aceder esta página." | |
564 | ||
565 | #: flask_security/templates/security/email/change_notice.html:1 | |
566 | msgid "Your password has been changed." | |
567 | msgstr "A sua palavra-passe foi alterada." | |
568 | ||
569 | #: flask_security/templates/security/email/change_notice.html:3 | |
570 | msgid "If you did not change your password," | |
571 | msgstr "Não alterou a sua palavra-passe," | |
572 | ||
573 | #: flask_security/templates/security/email/change_notice.html:3 | |
574 | msgid "click here to reset it" | |
575 | msgstr "clique aqui para redefinir" | |
576 | ||
577 | #: flask_security/templates/security/email/confirmation_instructions.html:1 | |
578 | msgid "Please confirm your email through the link below:" | |
579 | msgstr "Por favor, confirme o seu email através do endereço abaixo:" | |
580 | ||
581 | #: flask_security/templates/security/email/confirmation_instructions.html:3 | |
582 | #: flask_security/templates/security/email/welcome.html:6 | |
583 | msgid "Confirm my account" | |
584 | msgstr "Confirmar minha conta" | |
585 | ||
586 | #: flask_security/templates/security/email/login_instructions.html:1 | |
587 | #: flask_security/templates/security/email/welcome.html:1 | |
588 | #, python-format | |
589 | msgid "Welcome %(email)s!" | |
590 | msgstr "Bem-vindo %(email)s!" | |
591 | ||
592 | #: flask_security/templates/security/email/login_instructions.html:3 | |
593 | msgid "You can log into your account through the link below:" | |
594 | msgstr "Você pode iniciar sessão na sua conta através do endereço abaixo:" | |
595 | ||
596 | #: flask_security/templates/security/email/login_instructions.html:5 | |
597 | msgid "Login now" | |
598 | msgstr "Iniciar sessão" | |
599 | ||
600 | #: flask_security/templates/security/email/reset_instructions.html:1 | |
601 | msgid "Click here to reset your password" | |
602 | msgstr "Clique aqui para redefinir a sua palavra-passe" | |
603 | ||
604 | #: flask_security/templates/security/email/two_factor_instructions.html:3 | |
605 | msgid "You can log into your account using the following code:" | |
606 | msgstr "" | |
607 | ||
608 | #: flask_security/templates/security/email/two_factor_rescue.html:1 | |
609 | msgid "can not access mail account" | |
610 | msgstr "" | |
611 | ||
612 | #: flask_security/templates/security/email/us_instructions.html:3 | |
613 | #, fuzzy | |
614 | msgid "You can sign into your account using the following code:" | |
615 | msgstr "Você pode iniciar sessão na sua conta através do endereço abaixo:" | |
616 | ||
617 | #: flask_security/templates/security/email/us_instructions.html:6 | |
618 | #, fuzzy | |
619 | msgid "Or use the the link below:" | |
620 | msgstr "Você pode confirmar o seu email através do endereço abaixo:" | |
621 | ||
622 | #: flask_security/templates/security/email/welcome.html:4 | |
623 | msgid "You can confirm your email through the link below:" | |
624 | msgstr "Você pode confirmar o seu email através do endereço abaixo:" | |
625 |
+623
-0
0 | # Russian Translations for Flask-Security. | |
1 | # Copyright (C) 2017 CERN, leovp | |
2 | # This file is distributed under the same license as the Flask-Security | |
3 | # project. | |
4 | # FIRST AUTHOR <EMAIL@ADDRESS>, 2017. | |
5 | # | |
6 | msgid "" | |
7 | msgstr "" | |
8 | "Project-Id-Version: Flask-Security 2.0.1\n" | |
9 | "Report-Msgid-Bugs-To: [email protected]\n" | |
10 | "POT-Creation-Date: 2020-04-19 13:18-0700\n" | |
11 | "PO-Revision-Date: 2017-04-15 15:15+0300\n" | |
12 | "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | |
13 | "Language: ru_RU\n" | |
14 | "Language-Team: Leonid R. <[email protected]>\n" | |
15 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " | |
16 | "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" | |
17 | "MIME-Version: 1.0\n" | |
18 | "Content-Type: text/plain; charset=utf-8\n" | |
19 | "Content-Transfer-Encoding: 8bit\n" | |
20 | "Generated-By: Babel 2.8.0\n" | |
21 | ||
22 | #: flask_security/core.py:207 | |
23 | msgid "Login Required" | |
24 | msgstr "Требуется авторизация" | |
25 | ||
26 | #: flask_security/core.py:208 | |
27 | #: flask_security/templates/security/email/two_factor_instructions.html:1 | |
28 | #: flask_security/templates/security/email/us_instructions.html:1 | |
29 | msgid "Welcome" | |
30 | msgstr "Добро пожаловать" | |
31 | ||
32 | #: flask_security/core.py:209 | |
33 | msgid "Please confirm your email" | |
34 | msgstr "Пожалуйста, подтвердите свой почтовый адрес" | |
35 | ||
36 | #: flask_security/core.py:210 | |
37 | msgid "Login instructions" | |
38 | msgstr "Инструкция по входу" | |
39 | ||
40 | #: flask_security/core.py:211 | |
41 | #: flask_security/templates/security/email/reset_notice.html:1 | |
42 | msgid "Your password has been reset" | |
43 | msgstr "Ваш пароль был сброшен" | |
44 | ||
45 | #: flask_security/core.py:212 | |
46 | msgid "Your password has been changed" | |
47 | msgstr "Ваш пароль был изменён" | |
48 | ||
49 | #: flask_security/core.py:213 | |
50 | msgid "Password reset instructions" | |
51 | msgstr "Инструкция по восстановлению пароля" | |
52 | ||
53 | #: flask_security/core.py:216 | |
54 | msgid "Two-factor Login" | |
55 | msgstr "" | |
56 | ||
57 | #: flask_security/core.py:217 | |
58 | msgid "Two-factor Rescue" | |
59 | msgstr "" | |
60 | ||
61 | #: flask_security/core.py:266 | |
62 | msgid "Verification Code" | |
63 | msgstr "" | |
64 | ||
65 | #: flask_security/core.py:282 | |
66 | msgid "Input not appropriate for requested API" | |
67 | msgstr "" | |
68 | ||
69 | #: flask_security/core.py:283 | |
70 | msgid "You do not have permission to view this resource." | |
71 | msgstr "У вас нет прав доступа к этому ресурсу." | |
72 | ||
73 | #: flask_security/core.py:285 | |
74 | msgid "You are not authenticated. Please supply the correct credentials." | |
75 | msgstr "" | |
76 | ||
77 | #: flask_security/core.py:289 | |
78 | #, fuzzy | |
79 | msgid "You must re-authenticate to access this endpoint" | |
80 | msgstr "Пожалуйста, войдите заново чтобы получить доступ к этой странице." | |
81 | ||
82 | #: flask_security/core.py:293 | |
83 | #, python-format | |
84 | msgid "Thank you. Confirmation instructions have been sent to %(email)s." | |
85 | msgstr "Спасибо. Инструкция по подтверждению аккаунта отправлена на %(email)s." | |
86 | ||
87 | #: flask_security/core.py:296 | |
88 | msgid "Thank you. Your email has been confirmed." | |
89 | msgstr "Спасибо. Ваш почтовый адрес был подтверждён." | |
90 | ||
91 | #: flask_security/core.py:297 | |
92 | msgid "Your email has already been confirmed." | |
93 | msgstr "Ваш почтовый адрес уже подтверждён." | |
94 | ||
95 | #: flask_security/core.py:298 | |
96 | msgid "Invalid confirmation token." | |
97 | msgstr "Неверный токен для подтверждения аккаунта." | |
98 | ||
99 | #: flask_security/core.py:300 | |
100 | #, python-format | |
101 | msgid "%(email)s is already associated with an account." | |
102 | msgstr "%(email)s уже привязан к другому аккаунту." | |
103 | ||
104 | #: flask_security/core.py:303 | |
105 | msgid "Password does not match" | |
106 | msgstr "Пароль не подходит" | |
107 | ||
108 | #: flask_security/core.py:304 | |
109 | msgid "Passwords do not match" | |
110 | msgstr "Пароли не совпадают" | |
111 | ||
112 | #: flask_security/core.py:305 | |
113 | msgid "Redirections outside the domain are forbidden" | |
114 | msgstr "Перенаправления вне текущего домена запрещены" | |
115 | ||
116 | #: flask_security/core.py:307 | |
117 | #, python-format | |
118 | msgid "Instructions to reset your password have been sent to %(email)s." | |
119 | msgstr "Инструкция по восстановлению пароля отправлена на %(email)s." | |
120 | ||
121 | #: flask_security/core.py:311 | |
122 | #, python-format | |
123 | msgid "" | |
124 | "You did not reset your password within %(within)s. New instructions have " | |
125 | "been sent to %(email)s." | |
126 | msgstr "" | |
127 | "Вы не восстановили пароль в течение %(within)s. Новая инструкция " | |
128 | "отправлена на %(email)s." | |
129 | ||
130 | #: flask_security/core.py:317 | |
131 | msgid "Invalid reset password token." | |
132 | msgstr "Неверный токен для восстановления пароля." | |
133 | ||
134 | #: flask_security/core.py:318 | |
135 | msgid "Email requires confirmation." | |
136 | msgstr "Почтовый адрес требует подтверждения." | |
137 | ||
138 | #: flask_security/core.py:320 | |
139 | #, python-format | |
140 | msgid "Confirmation instructions have been sent to %(email)s." | |
141 | msgstr "Инструкция по подтверждению аккаунта отправлена на %(email)s." | |
142 | ||
143 | #: flask_security/core.py:324 | |
144 | #, python-format | |
145 | msgid "" | |
146 | "You did not confirm your email within %(within)s. New instructions to " | |
147 | "confirm your email have been sent to %(email)s." | |
148 | msgstr "" | |
149 | "Вы не подтвердили свой почтовый адрес в течение %(within)s. Новая " | |
150 | "инструкция по подтверждению отправлена на %(email)s." | |
151 | ||
152 | #: flask_security/core.py:332 | |
153 | #, python-format | |
154 | msgid "" | |
155 | "You did not login within %(within)s. New instructions to login have been " | |
156 | "sent to %(email)s." | |
157 | msgstr "" | |
158 | "Вы не вошли в течение %(within)s. Новая инструкция по входу отправлена на" | |
159 | " %(email)s." | |
160 | ||
161 | #: flask_security/core.py:339 | |
162 | #, python-format | |
163 | msgid "Instructions to login have been sent to %(email)s." | |
164 | msgstr "Инструкция по входу отправлена на %(email)s." | |
165 | ||
166 | #: flask_security/core.py:342 | |
167 | msgid "Invalid login token." | |
168 | msgstr "Неверный токен для входа." | |
169 | ||
170 | #: flask_security/core.py:343 | |
171 | msgid "Account is disabled." | |
172 | msgstr "Аккаунт отключён." | |
173 | ||
174 | #: flask_security/core.py:344 | |
175 | msgid "Email not provided" | |
176 | msgstr "Почтовый адрес не введён" | |
177 | ||
178 | #: flask_security/core.py:345 | |
179 | msgid "Invalid email address" | |
180 | msgstr "Неверный почтовый адрес" | |
181 | ||
182 | #: flask_security/core.py:346 | |
183 | #, fuzzy | |
184 | msgid "Invalid code" | |
185 | msgstr "Неверный пароль" | |
186 | ||
187 | #: flask_security/core.py:347 | |
188 | msgid "Password not provided" | |
189 | msgstr "Пароль не введён" | |
190 | ||
191 | #: flask_security/core.py:348 | |
192 | msgid "No password is set for this user" | |
193 | msgstr "У данного пользователя не установлен пароль" | |
194 | ||
195 | #: flask_security/core.py:350 | |
196 | #, fuzzy, python-format | |
197 | msgid "Password must be at least %(length)s characters" | |
198 | msgstr "Пароль должен содержать как минимум 6 символов" | |
199 | ||
200 | #: flask_security/core.py:353 | |
201 | msgid "Password not complex enough" | |
202 | msgstr "" | |
203 | ||
204 | #: flask_security/core.py:354 | |
205 | msgid "Password on breached list" | |
206 | msgstr "" | |
207 | ||
208 | #: flask_security/core.py:356 | |
209 | msgid "Failed to contact breached passwords site" | |
210 | msgstr "" | |
211 | ||
212 | #: flask_security/core.py:359 | |
213 | msgid "Phone number not valid e.g. missing country code" | |
214 | msgstr "" | |
215 | ||
216 | #: flask_security/core.py:360 | |
217 | msgid "Specified user does not exist" | |
218 | msgstr "Данный пользователь не найден" | |
219 | ||
220 | #: flask_security/core.py:361 | |
221 | msgid "Invalid password" | |
222 | msgstr "Неверный пароль" | |
223 | ||
224 | #: flask_security/core.py:362 | |
225 | msgid "Password or code submitted is not valid" | |
226 | msgstr "" | |
227 | ||
228 | #: flask_security/core.py:363 | |
229 | msgid "You have successfully logged in." | |
230 | msgstr "Вы вошли." | |
231 | ||
232 | #: flask_security/core.py:364 | |
233 | msgid "Forgot password?" | |
234 | msgstr "Забыли пароль?" | |
235 | ||
236 | #: flask_security/core.py:366 | |
237 | msgid "" | |
238 | "You successfully reset your password and you have been logged in " | |
239 | "automatically." | |
240 | msgstr "Ваш пароль был восстановлен и вы автоматически вошли." | |
241 | ||
242 | #: flask_security/core.py:373 | |
243 | msgid "Your new password must be different than your previous password." | |
244 | msgstr "Ваш новый пароль должен отличаться от предыдущего." | |
245 | ||
246 | #: flask_security/core.py:376 | |
247 | msgid "You successfully changed your password." | |
248 | msgstr "Вы удачно сменили пароль." | |
249 | ||
250 | #: flask_security/core.py:377 | |
251 | msgid "Please log in to access this page." | |
252 | msgstr "Пожалуйста, войдите чтобы получить доступ к этой странице." | |
253 | ||
254 | #: flask_security/core.py:378 | |
255 | msgid "Please reauthenticate to access this page." | |
256 | msgstr "Пожалуйста, войдите заново чтобы получить доступ к этой странице." | |
257 | ||
258 | #: flask_security/core.py:379 | |
259 | msgid "Reauthentication successful" | |
260 | msgstr "" | |
261 | ||
262 | #: flask_security/core.py:381 | |
263 | msgid "You can only access this endpoint when not logged in." | |
264 | msgstr "" | |
265 | ||
266 | #: flask_security/core.py:384 | |
267 | msgid "Failed to send code. Please try again later" | |
268 | msgstr "" | |
269 | ||
270 | #: flask_security/core.py:385 | |
271 | msgid "Invalid Token" | |
272 | msgstr "" | |
273 | ||
274 | #: flask_security/core.py:386 | |
275 | msgid "Your token has been confirmed" | |
276 | msgstr "" | |
277 | ||
278 | #: flask_security/core.py:388 | |
279 | msgid "You successfully changed your two-factor method." | |
280 | msgstr "" | |
281 | ||
282 | #: flask_security/core.py:392 | |
283 | msgid "You successfully confirmed password" | |
284 | msgstr "" | |
285 | ||
286 | #: flask_security/core.py:396 | |
287 | msgid "Password confirmation is needed in order to access page" | |
288 | msgstr "" | |
289 | ||
290 | #: flask_security/core.py:400 | |
291 | msgid "You currently do not have permissions to access this page" | |
292 | msgstr "" | |
293 | ||
294 | #: flask_security/core.py:403 | |
295 | msgid "Marked method is not valid" | |
296 | msgstr "" | |
297 | ||
298 | #: flask_security/core.py:405 | |
299 | msgid "You successfully disabled two factor authorization." | |
300 | msgstr "" | |
301 | ||
302 | #: flask_security/core.py:408 | |
303 | msgid "Requested method is not valid" | |
304 | msgstr "" | |
305 | ||
306 | #: flask_security/core.py:410 | |
307 | #, python-format | |
308 | msgid "Setup must be completed within %(within)s. Please start over." | |
309 | msgstr "" | |
310 | ||
311 | #: flask_security/core.py:413 | |
312 | msgid "Unified sign in setup successful" | |
313 | msgstr "" | |
314 | ||
315 | #: flask_security/core.py:414 | |
316 | msgid "You must specify a valid identity to sign in" | |
317 | msgstr "" | |
318 | ||
319 | #: flask_security/core.py:415 | |
320 | #, python-format | |
321 | msgid "Use this code to sign in: %(code)s." | |
322 | msgstr "" | |
323 | ||
324 | #: flask_security/forms.py:50 | |
325 | msgid "Email Address" | |
326 | msgstr "Почтовый адрес" | |
327 | ||
328 | #: flask_security/forms.py:51 | |
329 | msgid "Password" | |
330 | msgstr "Пароль" | |
331 | ||
332 | #: flask_security/forms.py:52 | |
333 | msgid "Remember Me" | |
334 | msgstr "Запомнить меня" | |
335 | ||
336 | #: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 | |
337 | #: flask_security/templates/security/login_user.html:6 | |
338 | #: flask_security/templates/security/send_login.html:6 | |
339 | msgid "Login" | |
340 | msgstr "Войти" | |
341 | ||
342 | #: flask_security/forms.py:54 | |
343 | #: flask_security/templates/security/email/us_instructions.html:8 | |
344 | #: flask_security/templates/security/us_signin.html:6 | |
345 | msgid "Sign In" | |
346 | msgstr "" | |
347 | ||
348 | #: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11 | |
349 | #: flask_security/templates/security/register_user.html:6 | |
350 | msgid "Register" | |
351 | msgstr "Зарегистрироваться" | |
352 | ||
353 | #: flask_security/forms.py:56 | |
354 | msgid "Resend Confirmation Instructions" | |
355 | msgstr "Заново отправить инструкцию по подтверждению аккаунта" | |
356 | ||
357 | #: flask_security/forms.py:57 | |
358 | msgid "Recover Password" | |
359 | msgstr "Восстановить пароль" | |
360 | ||
361 | #: flask_security/forms.py:58 | |
362 | msgid "Reset Password" | |
363 | msgstr "Сбросить пароль" | |
364 | ||
365 | #: flask_security/forms.py:59 | |
366 | msgid "Retype Password" | |
367 | msgstr "Подтверждение пароля" | |
368 | ||
369 | #: flask_security/forms.py:60 | |
370 | msgid "New Password" | |
371 | msgstr "Новый пароль" | |
372 | ||
373 | #: flask_security/forms.py:61 | |
374 | msgid "Change Password" | |
375 | msgstr "Сменить пароль" | |
376 | ||
377 | #: flask_security/forms.py:62 | |
378 | msgid "Send Login Link" | |
379 | msgstr "Отправить ссылку для входа" | |
380 | ||
381 | #: flask_security/forms.py:63 | |
382 | msgid "Verify Password" | |
383 | msgstr "" | |
384 | ||
385 | #: flask_security/forms.py:64 | |
386 | msgid "Change Method" | |
387 | msgstr "" | |
388 | ||
389 | #: flask_security/forms.py:65 | |
390 | msgid "Phone Number" | |
391 | msgstr "" | |
392 | ||
393 | #: flask_security/forms.py:66 | |
394 | msgid "Authentication Code" | |
395 | msgstr "" | |
396 | ||
397 | #: flask_security/forms.py:67 | |
398 | msgid "Submit" | |
399 | msgstr "" | |
400 | ||
401 | #: flask_security/forms.py:68 | |
402 | msgid "Submit Code" | |
403 | msgstr "" | |
404 | ||
405 | #: flask_security/forms.py:69 | |
406 | msgid "Error(s)" | |
407 | msgstr "" | |
408 | ||
409 | #: flask_security/forms.py:70 | |
410 | msgid "Identity" | |
411 | msgstr "" | |
412 | ||
413 | #: flask_security/forms.py:71 | |
414 | msgid "Send Code" | |
415 | msgstr "" | |
416 | ||
417 | #: flask_security/forms.py:72 | |
418 | #, fuzzy | |
419 | msgid "Passcode" | |
420 | msgstr "Пароль" | |
421 | ||
422 | #: flask_security/unified_signin.py:145 | |
423 | #, fuzzy | |
424 | msgid "Code or Password" | |
425 | msgstr "Восстановить пароль" | |
426 | ||
427 | #: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 | |
428 | msgid "Available Methods" | |
429 | msgstr "" | |
430 | ||
431 | #: flask_security/unified_signin.py:151 | |
432 | msgid "Via email" | |
433 | msgstr "" | |
434 | ||
435 | #: flask_security/unified_signin.py:151 | |
436 | msgid "Via SMS" | |
437 | msgstr "" | |
438 | ||
439 | #: flask_security/unified_signin.py:272 | |
440 | msgid "Set up using email" | |
441 | msgstr "" | |
442 | ||
443 | #: flask_security/unified_signin.py:275 | |
444 | msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" | |
445 | msgstr "" | |
446 | ||
447 | #: flask_security/unified_signin.py:277 | |
448 | msgid "Set up using SMS" | |
449 | msgstr "" | |
450 | ||
451 | #: flask_security/templates/security/_menu.html:2 | |
452 | msgid "Menu" | |
453 | msgstr "Меню" | |
454 | ||
455 | #: flask_security/templates/security/_menu.html:8 | |
456 | msgid "Unified Sign In" | |
457 | msgstr "" | |
458 | ||
459 | #: flask_security/templates/security/_menu.html:14 | |
460 | msgid "Forgot password" | |
461 | msgstr "Забыли пароль" | |
462 | ||
463 | #: flask_security/templates/security/_menu.html:17 | |
464 | msgid "Confirm account" | |
465 | msgstr "Подтвердить аккаунт" | |
466 | ||
467 | #: flask_security/templates/security/change_password.html:6 | |
468 | msgid "Change password" | |
469 | msgstr "Сменить пароль" | |
470 | ||
471 | #: flask_security/templates/security/forgot_password.html:6 | |
472 | msgid "Send password reset instructions" | |
473 | msgstr "Отправить инструкцию по сбросу пароля" | |
474 | ||
475 | #: flask_security/templates/security/reset_password.html:6 | |
476 | msgid "Reset password" | |
477 | msgstr "Сбросить пароль" | |
478 | ||
479 | #: flask_security/templates/security/send_confirmation.html:6 | |
480 | msgid "Resend confirmation instructions" | |
481 | msgstr "Заново отправить инструкцию по подтверждению аккаунта" | |
482 | ||
483 | #: flask_security/templates/security/two_factor_setup.html:6 | |
484 | msgid "Two-factor authentication adds an extra layer of security to your account" | |
485 | msgstr "" | |
486 | ||
487 | #: flask_security/templates/security/two_factor_setup.html:7 | |
488 | msgid "" | |
489 | "In addition to your username and password, you'll need to use a code that" | |
490 | " we will send you" | |
491 | msgstr "" | |
492 | ||
493 | #: flask_security/templates/security/two_factor_setup.html:18 | |
494 | msgid "To complete logging in, please enter the code sent to your mail" | |
495 | msgstr "" | |
496 | ||
497 | #: flask_security/templates/security/two_factor_setup.html:21 | |
498 | #: flask_security/templates/security/us_setup.html:21 | |
499 | msgid "" | |
500 | "Open your authenticator app on your device and scan the following qrcode " | |
501 | "to start receiving codes:" | |
502 | msgstr "" | |
503 | ||
504 | #: flask_security/templates/security/two_factor_setup.html:22 | |
505 | msgid "Two factor authentication code" | |
506 | msgstr "" | |
507 | ||
508 | #: flask_security/templates/security/two_factor_setup.html:25 | |
509 | msgid "To Which Phone Number Should We Send Code To?" | |
510 | msgstr "" | |
511 | ||
512 | #: flask_security/templates/security/two_factor_verify_code.html:6 | |
513 | msgid "Two-factor Authentication" | |
514 | msgstr "" | |
515 | ||
516 | #: flask_security/templates/security/two_factor_verify_code.html:7 | |
517 | msgid "Please enter your authentication code" | |
518 | msgstr "" | |
519 | ||
520 | #: flask_security/templates/security/two_factor_verify_code.html:18 | |
521 | msgid "The code for authentication was sent to your email address" | |
522 | msgstr "" | |
523 | ||
524 | #: flask_security/templates/security/two_factor_verify_code.html:21 | |
525 | msgid "A mail was sent to us in order to reset your application account" | |
526 | msgstr "" | |
527 | ||
528 | #: flask_security/templates/security/two_factor_verify_password.html:6 | |
529 | #: flask_security/templates/security/verify.html:6 | |
530 | msgid "Please Enter Your Password" | |
531 | msgstr "" | |
532 | ||
533 | #: flask_security/templates/security/us_setup.html:6 | |
534 | msgid "Setup Unified Sign In options" | |
535 | msgstr "" | |
536 | ||
537 | #: flask_security/templates/security/us_setup.html:22 | |
538 | msgid "Passwordless QRCode" | |
539 | msgstr "" | |
540 | ||
541 | #: flask_security/templates/security/us_setup.html:25 | |
542 | #: flask_security/templates/security/us_signin.html:23 | |
543 | #: flask_security/templates/security/us_verify.html:21 | |
544 | #, fuzzy | |
545 | msgid "Code has been sent" | |
546 | msgstr "Ваш пароль был сброшен" | |
547 | ||
548 | #: flask_security/templates/security/us_setup.html:29 | |
549 | msgid "No methods have been enabled - nothing to setup" | |
550 | msgstr "" | |
551 | ||
552 | #: flask_security/templates/security/us_signin.html:15 | |
553 | #: flask_security/templates/security/us_verify.html:13 | |
554 | msgid "Request one-time code be sent" | |
555 | msgstr "" | |
556 | ||
557 | #: flask_security/templates/security/us_verify.html:6 | |
558 | #, fuzzy | |
559 | msgid "Please re-authenticate" | |
560 | msgstr "Пожалуйста, войдите заново чтобы получить доступ к этой странице." | |
561 | ||
562 | #: flask_security/templates/security/email/change_notice.html:1 | |
563 | msgid "Your password has been changed." | |
564 | msgstr "Ваш пароль был изменён." | |
565 | ||
566 | #: flask_security/templates/security/email/change_notice.html:3 | |
567 | msgid "If you did not change your password," | |
568 | msgstr "Если вы не меняли свой пароль," | |
569 | ||
570 | #: flask_security/templates/security/email/change_notice.html:3 | |
571 | msgid "click here to reset it" | |
572 | msgstr "нажмите сюда чтобы сбросить его" | |
573 | ||
574 | #: flask_security/templates/security/email/confirmation_instructions.html:1 | |
575 | msgid "Please confirm your email through the link below:" | |
576 | msgstr "Пожалуйста, подтвердите свой почтовый адрес перейдя по ссылке:" | |
577 | ||
578 | #: flask_security/templates/security/email/confirmation_instructions.html:3 | |
579 | #: flask_security/templates/security/email/welcome.html:6 | |
580 | msgid "Confirm my account" | |
581 | msgstr "Подтвердить аккаунт" | |
582 | ||
583 | #: flask_security/templates/security/email/login_instructions.html:1 | |
584 | #: flask_security/templates/security/email/welcome.html:1 | |
585 | #, python-format | |
586 | msgid "Welcome %(email)s!" | |
587 | msgstr "Добро пожаловать, %(email)s!" | |
588 | ||
589 | #: flask_security/templates/security/email/login_instructions.html:3 | |
590 | msgid "You can log into your account through the link below:" | |
591 | msgstr "Вы можете войти по ссылке ниже:" | |
592 | ||
593 | #: flask_security/templates/security/email/login_instructions.html:5 | |
594 | msgid "Login now" | |
595 | msgstr "Войти" | |
596 | ||
597 | #: flask_security/templates/security/email/reset_instructions.html:1 | |
598 | msgid "Click here to reset your password" | |
599 | msgstr "Нажмите, чтобы сбросить свой пароль" | |
600 | ||
601 | #: flask_security/templates/security/email/two_factor_instructions.html:3 | |
602 | msgid "You can log into your account using the following code:" | |
603 | msgstr "" | |
604 | ||
605 | #: flask_security/templates/security/email/two_factor_rescue.html:1 | |
606 | msgid "can not access mail account" | |
607 | msgstr "" | |
608 | ||
609 | #: flask_security/templates/security/email/us_instructions.html:3 | |
610 | #, fuzzy | |
611 | msgid "You can sign into your account using the following code:" | |
612 | msgstr "Вы можете войти по ссылке ниже:" | |
613 | ||
614 | #: flask_security/templates/security/email/us_instructions.html:6 | |
615 | #, fuzzy | |
616 | msgid "Or use the the link below:" | |
617 | msgstr "Вы можете подтвердить свой почтовый адрес перейдя по ссылке:" | |
618 | ||
619 | #: flask_security/templates/security/email/welcome.html:4 | |
620 | msgid "You can confirm your email through the link below:" | |
621 | msgstr "Вы можете подтвердить свой почтовый адрес перейдя по ссылке:" | |
622 |
+621
-0
0 | # Turkish translation for Flask-Security. | |
1 | # Copyright (C) 2019 Ecmel B. Canlıer <[email protected]> | |
2 | # This file is distributed under the same license as the Flask-Security | |
3 | # project. | |
4 | # | |
5 | msgid "" | |
6 | msgstr "" | |
7 | "Project-Id-Version: Flask-Security 2.0.1\n" | |
8 | "Report-Msgid-Bugs-To: [email protected]\n" | |
9 | "POT-Creation-Date: 2020-04-19 13:18-0700\n" | |
10 | "PO-Revision-Date: 2018-12-20 18:48+0300\n" | |
11 | "Last-Translator: Ecmel B. Canlıer <[email protected]>\n" | |
12 | "Language: tr_TR\n" | |
13 | "Language-Team: \n" | |
14 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" | |
15 | "MIME-Version: 1.0\n" | |
16 | "Content-Type: text/plain; charset=utf-8\n" | |
17 | "Content-Transfer-Encoding: 8bit\n" | |
18 | "Generated-By: Babel 2.8.0\n" | |
19 | ||
20 | #: flask_security/core.py:207 | |
21 | msgid "Login Required" | |
22 | msgstr "Giriş yapmanız gerekmektedir" | |
23 | ||
24 | #: flask_security/core.py:208 | |
25 | #: flask_security/templates/security/email/two_factor_instructions.html:1 | |
26 | #: flask_security/templates/security/email/us_instructions.html:1 | |
27 | msgid "Welcome" | |
28 | msgstr "Hoş Geldiniz" | |
29 | ||
30 | #: flask_security/core.py:209 | |
31 | msgid "Please confirm your email" | |
32 | msgstr "Lütfen e-posta adresinizi onaylayın" | |
33 | ||
34 | #: flask_security/core.py:210 | |
35 | msgid "Login instructions" | |
36 | msgstr "Giriş talimatları" | |
37 | ||
38 | #: flask_security/core.py:211 | |
39 | #: flask_security/templates/security/email/reset_notice.html:1 | |
40 | msgid "Your password has been reset" | |
41 | msgstr "Şifreniz yenilenmiştir" | |
42 | ||
43 | #: flask_security/core.py:212 | |
44 | msgid "Your password has been changed" | |
45 | msgstr "Şifreniz değiştirilmiştir" | |
46 | ||
47 | #: flask_security/core.py:213 | |
48 | msgid "Password reset instructions" | |
49 | msgstr "Şifre yenileme talimatları" | |
50 | ||
51 | #: flask_security/core.py:216 | |
52 | msgid "Two-factor Login" | |
53 | msgstr "" | |
54 | ||
55 | #: flask_security/core.py:217 | |
56 | msgid "Two-factor Rescue" | |
57 | msgstr "" | |
58 | ||
59 | #: flask_security/core.py:266 | |
60 | msgid "Verification Code" | |
61 | msgstr "" | |
62 | ||
63 | #: flask_security/core.py:282 | |
64 | msgid "Input not appropriate for requested API" | |
65 | msgstr "" | |
66 | ||
67 | #: flask_security/core.py:283 | |
68 | msgid "You do not have permission to view this resource." | |
69 | msgstr "Bu maddeyi görmeye yetkiniz yoktur." | |
70 | ||
71 | #: flask_security/core.py:285 | |
72 | msgid "You are not authenticated. Please supply the correct credentials." | |
73 | msgstr "" | |
74 | ||
75 | #: flask_security/core.py:289 | |
76 | #, fuzzy | |
77 | msgid "You must re-authenticate to access this endpoint" | |
78 | msgstr "Bu sayfaya erişebilmek için lütfen tekrardan giriş yapın." | |
79 | ||
80 | #: flask_security/core.py:293 | |
81 | #, python-format | |
82 | msgid "Thank you. Confirmation instructions have been sent to %(email)s." | |
83 | msgstr "Teşekkür ederiz. Onaylama talimatları %(email)s adresine gönderilmiştir." | |
84 | ||
85 | #: flask_security/core.py:296 | |
86 | msgid "Thank you. Your email has been confirmed." | |
87 | msgstr "Teşekkür ederiz. E-posta adresiniz onaylanmıştır" | |
88 | ||
89 | #: flask_security/core.py:297 | |
90 | msgid "Your email has already been confirmed." | |
91 | msgstr "E-posta adresiniz zaten onaylanmış." | |
92 | ||
93 | #: flask_security/core.py:298 | |
94 | msgid "Invalid confirmation token." | |
95 | msgstr "Yanlış onaylama kodu." | |
96 | ||
97 | #: flask_security/core.py:300 | |
98 | #, python-format | |
99 | msgid "%(email)s is already associated with an account." | |
100 | msgstr "%(email)s başka bir hesaba bağlı." | |
101 | ||
102 | #: flask_security/core.py:303 | |
103 | msgid "Password does not match" | |
104 | msgstr "Şifre yanlış" | |
105 | ||
106 | #: flask_security/core.py:304 | |
107 | msgid "Passwords do not match" | |
108 | msgstr "Şifreler uymuyor" | |
109 | ||
110 | #: flask_security/core.py:305 | |
111 | msgid "Redirections outside the domain are forbidden" | |
112 | msgstr "Adres dışına yönlendirmeler yasaktır" | |
113 | ||
114 | #: flask_security/core.py:307 | |
115 | #, python-format | |
116 | msgid "Instructions to reset your password have been sent to %(email)s." | |
117 | msgstr "Şifrenizi yenileme talimatları %(email)s adresine gönderilmiştir." | |
118 | ||
119 | #: flask_security/core.py:311 | |
120 | #, python-format | |
121 | msgid "" | |
122 | "You did not reset your password within %(within)s. New instructions have " | |
123 | "been sent to %(email)s." | |
124 | msgstr "" | |
125 | "Şifrenizi %(within)s içinde yenilemediniz. Yeni talimatlar %(email)s " | |
126 | "adresine gönderilmiştir." | |
127 | ||
128 | #: flask_security/core.py:317 | |
129 | msgid "Invalid reset password token." | |
130 | msgstr "Yanlış şifre yenileme kodu." | |
131 | ||
132 | #: flask_security/core.py:318 | |
133 | msgid "Email requires confirmation." | |
134 | msgstr "E-posta onayı gerekmektedir." | |
135 | ||
136 | #: flask_security/core.py:320 | |
137 | #, python-format | |
138 | msgid "Confirmation instructions have been sent to %(email)s." | |
139 | msgstr "Onaylama talimatları %(email)s adresine gönderilmiştir." | |
140 | ||
141 | #: flask_security/core.py:324 | |
142 | #, python-format | |
143 | msgid "" | |
144 | "You did not confirm your email within %(within)s. New instructions to " | |
145 | "confirm your email have been sent to %(email)s." | |
146 | msgstr "" | |
147 | "E-posta adresinizi %(within)s içinde onaylamadınız. Yeni onaylama " | |
148 | "talimatları %(email)s adresine gönderilmiştir." | |
149 | ||
150 | #: flask_security/core.py:332 | |
151 | #, python-format | |
152 | msgid "" | |
153 | "You did not login within %(within)s. New instructions to login have been " | |
154 | "sent to %(email)s." | |
155 | msgstr "" | |
156 | "%(within)s içinde giriş yapmadınız. Yeni giriş yapma talimatları " | |
157 | "%(email)s adresine gönderilmiştir." | |
158 | ||
159 | #: flask_security/core.py:339 | |
160 | #, python-format | |
161 | msgid "Instructions to login have been sent to %(email)s." | |
162 | msgstr "Giriş yapma talimatları %(email)s adresine gönderilmiştir." | |
163 | ||
164 | #: flask_security/core.py:342 | |
165 | msgid "Invalid login token." | |
166 | msgstr "Yanlış giriş kodu." | |
167 | ||
168 | #: flask_security/core.py:343 | |
169 | msgid "Account is disabled." | |
170 | msgstr "Hesap kapalıdır." | |
171 | ||
172 | #: flask_security/core.py:344 | |
173 | msgid "Email not provided" | |
174 | msgstr "E-posta verilmemiş" | |
175 | ||
176 | #: flask_security/core.py:345 | |
177 | msgid "Invalid email address" | |
178 | msgstr "Yanlış e-posta adresi" | |
179 | ||
180 | #: flask_security/core.py:346 | |
181 | #, fuzzy | |
182 | msgid "Invalid code" | |
183 | msgstr "Şifre yanlış" | |
184 | ||
185 | #: flask_security/core.py:347 | |
186 | msgid "Password not provided" | |
187 | msgstr "Şifre verilmemiş" | |
188 | ||
189 | #: flask_security/core.py:348 | |
190 | msgid "No password is set for this user" | |
191 | msgstr "Bu kullanıcı için bir şifre yok" | |
192 | ||
193 | #: flask_security/core.py:350 | |
194 | #, fuzzy, python-format | |
195 | msgid "Password must be at least %(length)s characters" | |
196 | msgstr "Şifreniz en az 6 karakter olmalıdır" | |
197 | ||
198 | #: flask_security/core.py:353 | |
199 | msgid "Password not complex enough" | |
200 | msgstr "" | |
201 | ||
202 | #: flask_security/core.py:354 | |
203 | msgid "Password on breached list" | |
204 | msgstr "" | |
205 | ||
206 | #: flask_security/core.py:356 | |
207 | msgid "Failed to contact breached passwords site" | |
208 | msgstr "" | |
209 | ||
210 | #: flask_security/core.py:359 | |
211 | msgid "Phone number not valid e.g. missing country code" | |
212 | msgstr "" | |
213 | ||
214 | #: flask_security/core.py:360 | |
215 | msgid "Specified user does not exist" | |
216 | msgstr "Böyle bir kullanıcı yok" | |
217 | ||
218 | #: flask_security/core.py:361 | |
219 | msgid "Invalid password" | |
220 | msgstr "Şifre yanlış" | |
221 | ||
222 | #: flask_security/core.py:362 | |
223 | msgid "Password or code submitted is not valid" | |
224 | msgstr "" | |
225 | ||
226 | #: flask_security/core.py:363 | |
227 | msgid "You have successfully logged in." | |
228 | msgstr "Başarıyla giriş yaptınız." | |
229 | ||
230 | #: flask_security/core.py:364 | |
231 | msgid "Forgot password?" | |
232 | msgstr "Şifrenizi mi unuttunuz?" | |
233 | ||
234 | #: flask_security/core.py:366 | |
235 | msgid "" | |
236 | "You successfully reset your password and you have been logged in " | |
237 | "automatically." | |
238 | msgstr "Şifreniz yenilenmiştir ve otomatik olarak giriş yapmış bulunmaktasınız." | |
239 | ||
240 | #: flask_security/core.py:373 | |
241 | msgid "Your new password must be different than your previous password." | |
242 | msgstr "Yeni şifreniz eski şifrenizden farklı olmalıdır." | |
243 | ||
244 | #: flask_security/core.py:376 | |
245 | msgid "You successfully changed your password." | |
246 | msgstr "Şifrenizi başarıyla değiştirdiniz." | |
247 | ||
248 | #: flask_security/core.py:377 | |
249 | msgid "Please log in to access this page." | |
250 | msgstr "Bu sayfaya erişebilmek için lütfen giriş yapın." | |
251 | ||
252 | #: flask_security/core.py:378 | |
253 | msgid "Please reauthenticate to access this page." | |
254 | msgstr "Bu sayfaya erişebilmek için lütfen tekrardan giriş yapın." | |
255 | ||
256 | #: flask_security/core.py:379 | |
257 | msgid "Reauthentication successful" | |
258 | msgstr "" | |
259 | ||
260 | #: flask_security/core.py:381 | |
261 | msgid "You can only access this endpoint when not logged in." | |
262 | msgstr "" | |
263 | ||
264 | #: flask_security/core.py:384 | |
265 | msgid "Failed to send code. Please try again later" | |
266 | msgstr "" | |
267 | ||
268 | #: flask_security/core.py:385 | |
269 | msgid "Invalid Token" | |
270 | msgstr "" | |
271 | ||
272 | #: flask_security/core.py:386 | |
273 | msgid "Your token has been confirmed" | |
274 | msgstr "" | |
275 | ||
276 | #: flask_security/core.py:388 | |
277 | msgid "You successfully changed your two-factor method." | |
278 | msgstr "" | |
279 | ||
280 | #: flask_security/core.py:392 | |
281 | msgid "You successfully confirmed password" | |
282 | msgstr "" | |
283 | ||
284 | #: flask_security/core.py:396 | |
285 | msgid "Password confirmation is needed in order to access page" | |
286 | msgstr "" | |
287 | ||
288 | #: flask_security/core.py:400 | |
289 | msgid "You currently do not have permissions to access this page" | |
290 | msgstr "" | |
291 | ||
292 | #: flask_security/core.py:403 | |
293 | msgid "Marked method is not valid" | |
294 | msgstr "" | |
295 | ||
296 | #: flask_security/core.py:405 | |
297 | msgid "You successfully disabled two factor authorization." | |
298 | msgstr "" | |
299 | ||
300 | #: flask_security/core.py:408 | |
301 | msgid "Requested method is not valid" | |
302 | msgstr "" | |
303 | ||
304 | #: flask_security/core.py:410 | |
305 | #, python-format | |
306 | msgid "Setup must be completed within %(within)s. Please start over." | |
307 | msgstr "" | |
308 | ||
309 | #: flask_security/core.py:413 | |
310 | msgid "Unified sign in setup successful" | |
311 | msgstr "" | |
312 | ||
313 | #: flask_security/core.py:414 | |
314 | msgid "You must specify a valid identity to sign in" | |
315 | msgstr "" | |
316 | ||
317 | #: flask_security/core.py:415 | |
318 | #, python-format | |
319 | msgid "Use this code to sign in: %(code)s." | |
320 | msgstr "" | |
321 | ||
322 | #: flask_security/forms.py:50 | |
323 | msgid "Email Address" | |
324 | msgstr "E-posta Adresi" | |
325 | ||
326 | #: flask_security/forms.py:51 | |
327 | msgid "Password" | |
328 | msgstr "Şifre" | |
329 | ||
330 | #: flask_security/forms.py:52 | |
331 | msgid "Remember Me" | |
332 | msgstr "Beni Hatırla" | |
333 | ||
334 | #: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 | |
335 | #: flask_security/templates/security/login_user.html:6 | |
336 | #: flask_security/templates/security/send_login.html:6 | |
337 | msgid "Login" | |
338 | msgstr "Giriş Yap" | |
339 | ||
340 | #: flask_security/forms.py:54 | |
341 | #: flask_security/templates/security/email/us_instructions.html:8 | |
342 | #: flask_security/templates/security/us_signin.html:6 | |
343 | msgid "Sign In" | |
344 | msgstr "" | |
345 | ||
346 | #: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11 | |
347 | #: flask_security/templates/security/register_user.html:6 | |
348 | msgid "Register" | |
349 | msgstr "Kayıt Ol" | |
350 | ||
351 | #: flask_security/forms.py:56 | |
352 | msgid "Resend Confirmation Instructions" | |
353 | msgstr "Onaylama Talimatlarını Tekrar Gönder" | |
354 | ||
355 | #: flask_security/forms.py:57 | |
356 | msgid "Recover Password" | |
357 | msgstr "Şifre Kurtar" | |
358 | ||
359 | #: flask_security/forms.py:58 | |
360 | msgid "Reset Password" | |
361 | msgstr "Şifre Yenile" | |
362 | ||
363 | #: flask_security/forms.py:59 | |
364 | msgid "Retype Password" | |
365 | msgstr "Şifre Tekrarı" | |
366 | ||
367 | #: flask_security/forms.py:60 | |
368 | msgid "New Password" | |
369 | msgstr "Yeni Şifre" | |
370 | ||
371 | #: flask_security/forms.py:61 | |
372 | msgid "Change Password" | |
373 | msgstr "Şifre Değiştir" | |
374 | ||
375 | #: flask_security/forms.py:62 | |
376 | msgid "Send Login Link" | |
377 | msgstr "Giriş Linki Gönder" | |
378 | ||
379 | #: flask_security/forms.py:63 | |
380 | msgid "Verify Password" | |
381 | msgstr "" | |
382 | ||
383 | #: flask_security/forms.py:64 | |
384 | msgid "Change Method" | |
385 | msgstr "" | |
386 | ||
387 | #: flask_security/forms.py:65 | |
388 | msgid "Phone Number" | |
389 | msgstr "" | |
390 | ||
391 | #: flask_security/forms.py:66 | |
392 | msgid "Authentication Code" | |
393 | msgstr "" | |
394 | ||
395 | #: flask_security/forms.py:67 | |
396 | msgid "Submit" | |
397 | msgstr "" | |
398 | ||
399 | #: flask_security/forms.py:68 | |
400 | msgid "Submit Code" | |
401 | msgstr "" | |
402 | ||
403 | #: flask_security/forms.py:69 | |
404 | msgid "Error(s)" | |
405 | msgstr "" | |
406 | ||
407 | #: flask_security/forms.py:70 | |
408 | msgid "Identity" | |
409 | msgstr "" | |
410 | ||
411 | #: flask_security/forms.py:71 | |
412 | msgid "Send Code" | |
413 | msgstr "" | |
414 | ||
415 | #: flask_security/forms.py:72 | |
416 | #, fuzzy | |
417 | msgid "Passcode" | |
418 | msgstr "Şifre" | |
419 | ||
420 | #: flask_security/unified_signin.py:145 | |
421 | #, fuzzy | |
422 | msgid "Code or Password" | |
423 | msgstr "Şifre Kurtar" | |
424 | ||
425 | #: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 | |
426 | msgid "Available Methods" | |
427 | msgstr "" | |
428 | ||
429 | #: flask_security/unified_signin.py:151 | |
430 | msgid "Via email" | |
431 | msgstr "" | |
432 | ||
433 | #: flask_security/unified_signin.py:151 | |
434 | msgid "Via SMS" | |
435 | msgstr "" | |
436 | ||
437 | #: flask_security/unified_signin.py:272 | |
438 | msgid "Set up using email" | |
439 | msgstr "" | |
440 | ||
441 | #: flask_security/unified_signin.py:275 | |
442 | msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" | |
443 | msgstr "" | |
444 | ||
445 | #: flask_security/unified_signin.py:277 | |
446 | msgid "Set up using SMS" | |
447 | msgstr "" | |
448 | ||
449 | #: flask_security/templates/security/_menu.html:2 | |
450 | msgid "Menu" | |
451 | msgstr "Menü" | |
452 | ||
453 | #: flask_security/templates/security/_menu.html:8 | |
454 | msgid "Unified Sign In" | |
455 | msgstr "" | |
456 | ||
457 | #: flask_security/templates/security/_menu.html:14 | |
458 | msgid "Forgot password" | |
459 | msgstr "Şifremi unuttum" | |
460 | ||
461 | #: flask_security/templates/security/_menu.html:17 | |
462 | msgid "Confirm account" | |
463 | msgstr "Hesabı onayla" | |
464 | ||
465 | #: flask_security/templates/security/change_password.html:6 | |
466 | msgid "Change password" | |
467 | msgstr "Şifre değiştir" | |
468 | ||
469 | #: flask_security/templates/security/forgot_password.html:6 | |
470 | msgid "Send password reset instructions" | |
471 | msgstr "Şifre değiştirme talimatlarını gönder" | |
472 | ||
473 | #: flask_security/templates/security/reset_password.html:6 | |
474 | msgid "Reset password" | |
475 | msgstr "Şifre yenile" | |
476 | ||
477 | #: flask_security/templates/security/send_confirmation.html:6 | |
478 | msgid "Resend confirmation instructions" | |
479 | msgstr "Onaylama talimatlarını tekrar gönder" | |
480 | ||
481 | #: flask_security/templates/security/two_factor_setup.html:6 | |
482 | msgid "Two-factor authentication adds an extra layer of security to your account" | |
483 | msgstr "" | |
484 | ||
485 | #: flask_security/templates/security/two_factor_setup.html:7 | |
486 | msgid "" | |
487 | "In addition to your username and password, you'll need to use a code that" | |
488 | " we will send you" | |
489 | msgstr "" | |
490 | ||
491 | #: flask_security/templates/security/two_factor_setup.html:18 | |
492 | msgid "To complete logging in, please enter the code sent to your mail" | |
493 | msgstr "" | |
494 | ||
495 | #: flask_security/templates/security/two_factor_setup.html:21 | |
496 | #: flask_security/templates/security/us_setup.html:21 | |
497 | msgid "" | |
498 | "Open your authenticator app on your device and scan the following qrcode " | |
499 | "to start receiving codes:" | |
500 | msgstr "" | |
501 | ||
502 | #: flask_security/templates/security/two_factor_setup.html:22 | |
503 | msgid "Two factor authentication code" | |
504 | msgstr "" | |
505 | ||
506 | #: flask_security/templates/security/two_factor_setup.html:25 | |
507 | msgid "To Which Phone Number Should We Send Code To?" | |
508 | msgstr "" | |
509 | ||
510 | #: flask_security/templates/security/two_factor_verify_code.html:6 | |
511 | msgid "Two-factor Authentication" | |
512 | msgstr "" | |
513 | ||
514 | #: flask_security/templates/security/two_factor_verify_code.html:7 | |
515 | msgid "Please enter your authentication code" | |
516 | msgstr "" | |
517 | ||
518 | #: flask_security/templates/security/two_factor_verify_code.html:18 | |
519 | msgid "The code for authentication was sent to your email address" | |
520 | msgstr "" | |
521 | ||
522 | #: flask_security/templates/security/two_factor_verify_code.html:21 | |
523 | msgid "A mail was sent to us in order to reset your application account" | |
524 | msgstr "" | |
525 | ||
526 | #: flask_security/templates/security/two_factor_verify_password.html:6 | |
527 | #: flask_security/templates/security/verify.html:6 | |
528 | msgid "Please Enter Your Password" | |
529 | msgstr "" | |
530 | ||
531 | #: flask_security/templates/security/us_setup.html:6 | |
532 | msgid "Setup Unified Sign In options" | |
533 | msgstr "" | |
534 | ||
535 | #: flask_security/templates/security/us_setup.html:22 | |
536 | msgid "Passwordless QRCode" | |
537 | msgstr "" | |
538 | ||
539 | #: flask_security/templates/security/us_setup.html:25 | |
540 | #: flask_security/templates/security/us_signin.html:23 | |
541 | #: flask_security/templates/security/us_verify.html:21 | |
542 | #, fuzzy | |
543 | msgid "Code has been sent" | |
544 | msgstr "Şifreniz yenilenmiştir" | |
545 | ||
546 | #: flask_security/templates/security/us_setup.html:29 | |
547 | msgid "No methods have been enabled - nothing to setup" | |
548 | msgstr "" | |
549 | ||
550 | #: flask_security/templates/security/us_signin.html:15 | |
551 | #: flask_security/templates/security/us_verify.html:13 | |
552 | msgid "Request one-time code be sent" | |
553 | msgstr "" | |
554 | ||
555 | #: flask_security/templates/security/us_verify.html:6 | |
556 | #, fuzzy | |
557 | msgid "Please re-authenticate" | |
558 | msgstr "Bu sayfaya erişebilmek için lütfen tekrardan giriş yapın." | |
559 | ||
560 | #: flask_security/templates/security/email/change_notice.html:1 | |
561 | msgid "Your password has been changed." | |
562 | msgstr "Şifreniz değiştirilmiştir." | |
563 | ||
564 | #: flask_security/templates/security/email/change_notice.html:3 | |
565 | msgid "If you did not change your password," | |
566 | msgstr "Eğer siz değiştirmediyseniz," | |
567 | ||
568 | #: flask_security/templates/security/email/change_notice.html:3 | |
569 | msgid "click here to reset it" | |
570 | msgstr "buraya tıklayarak yenileyiniz" | |
571 | ||
572 | #: flask_security/templates/security/email/confirmation_instructions.html:1 | |
573 | msgid "Please confirm your email through the link below:" | |
574 | msgstr "Lütfen e-posta adresinizi aşağıdaki linkten onaylayınız:" | |
575 | ||
576 | #: flask_security/templates/security/email/confirmation_instructions.html:3 | |
577 | #: flask_security/templates/security/email/welcome.html:6 | |
578 | msgid "Confirm my account" | |
579 | msgstr "Hesabımı onayla" | |
580 | ||
581 | #: flask_security/templates/security/email/login_instructions.html:1 | |
582 | #: flask_security/templates/security/email/welcome.html:1 | |
583 | #, python-format | |
584 | msgid "Welcome %(email)s!" | |
585 | msgstr "Hoş Geldin %(email)s" | |
586 | ||
587 | #: flask_security/templates/security/email/login_instructions.html:3 | |
588 | msgid "You can log into your account through the link below:" | |
589 | msgstr "Hesabına aşağıdaki linkten giriş yapabilirsin:" | |
590 | ||
591 | #: flask_security/templates/security/email/login_instructions.html:5 | |
592 | msgid "Login now" | |
593 | msgstr "Şimdi giriş yap" | |
594 | ||
595 | #: flask_security/templates/security/email/reset_instructions.html:1 | |
596 | msgid "Click here to reset your password" | |
597 | msgstr "Şifreni yenilemek için buraya tıkla" | |
598 | ||
599 | #: flask_security/templates/security/email/two_factor_instructions.html:3 | |
600 | msgid "You can log into your account using the following code:" | |
601 | msgstr "" | |
602 | ||
603 | #: flask_security/templates/security/email/two_factor_rescue.html:1 | |
604 | msgid "can not access mail account" | |
605 | msgstr "" | |
606 | ||
607 | #: flask_security/templates/security/email/us_instructions.html:3 | |
608 | #, fuzzy | |
609 | msgid "You can sign into your account using the following code:" | |
610 | msgstr "Hesabına aşağıdaki linkten giriş yapabilirsin:" | |
611 | ||
612 | #: flask_security/templates/security/email/us_instructions.html:6 | |
613 | #, fuzzy | |
614 | msgid "Or use the the link below:" | |
615 | msgstr "E-posta adresinizi aşağıdaki linkten onaylayabilirsiniz:" | |
616 | ||
617 | #: flask_security/templates/security/email/welcome.html:4 | |
618 | msgid "You can confirm your email through the link below:" | |
619 | msgstr "E-posta adresinizi aşağıdaki linkten onaylayabilirsiniz:" | |
620 |
+616
-0
0 | # Chinese (Simplified, China) translations for Flask-Security. | |
1 | # Copyright (C) 2017 CERN | |
2 | # This file is distributed under the same license as the Flask-Security | |
3 | # project. | |
4 | # FIRST AUTHOR <EMAIL@ADDRESS>, 2017. | |
5 | # | |
6 | msgid "" | |
7 | msgstr "" | |
8 | "Project-Id-Version: Flask-Security 2.0.1\n" | |
9 | "Report-Msgid-Bugs-To: [email protected]\n" | |
10 | "POT-Creation-Date: 2020-04-19 13:18-0700\n" | |
11 | "PO-Revision-Date: 2018-08-02 19:55+0800\n" | |
12 | "Last-Translator: SteinKuo <[email protected]>\n" | |
13 | "Language: zh_CN\n" | |
14 | "Language-Team: Chinese Simplified <[email protected]>\n" | |
15 | "Plural-Forms: nplurals=1; plural=0\n" | |
16 | "MIME-Version: 1.0\n" | |
17 | "Content-Type: text/plain; charset=utf-8\n" | |
18 | "Content-Transfer-Encoding: 8bit\n" | |
19 | "Generated-By: Babel 2.8.0\n" | |
20 | ||
21 | #: flask_security/core.py:207 | |
22 | msgid "Login Required" | |
23 | msgstr "需要登录" | |
24 | ||
25 | #: flask_security/core.py:208 | |
26 | #: flask_security/templates/security/email/two_factor_instructions.html:1 | |
27 | #: flask_security/templates/security/email/us_instructions.html:1 | |
28 | msgid "Welcome" | |
29 | msgstr "欢迎" | |
30 | ||
31 | #: flask_security/core.py:209 | |
32 | msgid "Please confirm your email" | |
33 | msgstr "请激活你的电子邮箱" | |
34 | ||
35 | #: flask_security/core.py:210 | |
36 | msgid "Login instructions" | |
37 | msgstr "登录邮件" | |
38 | ||
39 | #: flask_security/core.py:211 | |
40 | #: flask_security/templates/security/email/reset_notice.html:1 | |
41 | msgid "Your password has been reset" | |
42 | msgstr "你的密码已重置" | |
43 | ||
44 | #: flask_security/core.py:212 | |
45 | msgid "Your password has been changed" | |
46 | msgstr "你的密码已更改" | |
47 | ||
48 | #: flask_security/core.py:213 | |
49 | msgid "Password reset instructions" | |
50 | msgstr "密码重置" | |
51 | ||
52 | #: flask_security/core.py:216 | |
53 | msgid "Two-factor Login" | |
54 | msgstr "" | |
55 | ||
56 | #: flask_security/core.py:217 | |
57 | msgid "Two-factor Rescue" | |
58 | msgstr "" | |
59 | ||
60 | #: flask_security/core.py:266 | |
61 | msgid "Verification Code" | |
62 | msgstr "" | |
63 | ||
64 | #: flask_security/core.py:282 | |
65 | msgid "Input not appropriate for requested API" | |
66 | msgstr "" | |
67 | ||
68 | #: flask_security/core.py:283 | |
69 | msgid "You do not have permission to view this resource." | |
70 | msgstr "你无权查看此资源!" | |
71 | ||
72 | #: flask_security/core.py:285 | |
73 | msgid "You are not authenticated. Please supply the correct credentials." | |
74 | msgstr "" | |
75 | ||
76 | #: flask_security/core.py:289 | |
77 | #, fuzzy | |
78 | msgid "You must re-authenticate to access this endpoint" | |
79 | msgstr "请重新进行身份验证,以访问此页面。" | |
80 | ||
81 | #: flask_security/core.py:293 | |
82 | #, python-format | |
83 | msgid "Thank you. Confirmation instructions have been sent to %(email)s." | |
84 | msgstr "谢谢你。已发送激活邮件到 %(email)s。" | |
85 | ||
86 | #: flask_security/core.py:296 | |
87 | msgid "Thank you. Your email has been confirmed." | |
88 | msgstr "谢谢。你的邮箱已激活!" | |
89 | ||
90 | #: flask_security/core.py:297 | |
91 | msgid "Your email has already been confirmed." | |
92 | msgstr "你的邮箱已激活!" | |
93 | ||
94 | #: flask_security/core.py:298 | |
95 | msgid "Invalid confirmation token." | |
96 | msgstr "无效验证码!" | |
97 | ||
98 | #: flask_security/core.py:300 | |
99 | #, python-format | |
100 | msgid "%(email)s is already associated with an account." | |
101 | msgstr "%(email)s 已关联账户。" | |
102 | ||
103 | #: flask_security/core.py:303 | |
104 | msgid "Password does not match" | |
105 | msgstr "密码不匹配" | |
106 | ||
107 | #: flask_security/core.py:304 | |
108 | msgid "Passwords do not match" | |
109 | msgstr "密码不匹配" | |
110 | ||
111 | #: flask_security/core.py:305 | |
112 | msgid "Redirections outside the domain are forbidden" | |
113 | msgstr "禁止域名外重定向" | |
114 | ||
115 | #: flask_security/core.py:307 | |
116 | #, python-format | |
117 | msgid "Instructions to reset your password have been sent to %(email)s." | |
118 | msgstr "重置密码邮件已发送到 %(email)s。" | |
119 | ||
120 | #: flask_security/core.py:311 | |
121 | #, python-format | |
122 | msgid "" | |
123 | "You did not reset your password within %(within)s. New instructions have " | |
124 | "been sent to %(email)s." | |
125 | msgstr "你未在 %(within)s 重置密码。新重置密码邮件已发送到 %(email)s。" | |
126 | ||
127 | #: flask_security/core.py:317 | |
128 | msgid "Invalid reset password token." | |
129 | msgstr "密码重置验证码无效!" | |
130 | ||
131 | #: flask_security/core.py:318 | |
132 | msgid "Email requires confirmation." | |
133 | msgstr "请先激活邮箱。" | |
134 | ||
135 | #: flask_security/core.py:320 | |
136 | #, python-format | |
137 | msgid "Confirmation instructions have been sent to %(email)s." | |
138 | msgstr "激活邮件已发送到 %(email)s。" | |
139 | ||
140 | #: flask_security/core.py:324 | |
141 | #, python-format | |
142 | msgid "" | |
143 | "You did not confirm your email within %(within)s. New instructions to " | |
144 | "confirm your email have been sent to %(email)s." | |
145 | msgstr "你未在 %(within)s 激活邮箱。新激活邮件已发送到 %(email)s。" | |
146 | ||
147 | #: flask_security/core.py:332 | |
148 | #, python-format | |
149 | msgid "" | |
150 | "You did not login within %(within)s. New instructions to login have been " | |
151 | "sent to %(email)s." | |
152 | msgstr "你未在 %(within)s 登录账户。新登录邮件已发送到 %(email)s。" | |
153 | ||
154 | #: flask_security/core.py:339 | |
155 | #, python-format | |
156 | msgid "Instructions to login have been sent to %(email)s." | |
157 | msgstr "登录邮件已发送到 %(email)s。" | |
158 | ||
159 | #: flask_security/core.py:342 | |
160 | msgid "Invalid login token." | |
161 | msgstr "无效登录验证码!" | |
162 | ||
163 | #: flask_security/core.py:343 | |
164 | msgid "Account is disabled." | |
165 | msgstr "账户已被禁用!" | |
166 | ||
167 | #: flask_security/core.py:344 | |
168 | msgid "Email not provided" | |
169 | msgstr "未填写电子邮箱" | |
170 | ||
171 | #: flask_security/core.py:345 | |
172 | msgid "Invalid email address" | |
173 | msgstr "无效邮箱地址" | |
174 | ||
175 | #: flask_security/core.py:346 | |
176 | #, fuzzy | |
177 | msgid "Invalid code" | |
178 | msgstr "密码不正确" | |
179 | ||
180 | #: flask_security/core.py:347 | |
181 | msgid "Password not provided" | |
182 | msgstr "未填写密码" | |
183 | ||
184 | #: flask_security/core.py:348 | |
185 | msgid "No password is set for this user" | |
186 | msgstr "此账户未设置密码" | |
187 | ||
188 | #: flask_security/core.py:350 | |
189 | #, fuzzy, python-format | |
190 | msgid "Password must be at least %(length)s characters" | |
191 | msgstr "密码至少6个字符" | |
192 | ||
193 | #: flask_security/core.py:353 | |
194 | msgid "Password not complex enough" | |
195 | msgstr "" | |
196 | ||
197 | #: flask_security/core.py:354 | |
198 | msgid "Password on breached list" | |
199 | msgstr "" | |
200 | ||
201 | #: flask_security/core.py:356 | |
202 | msgid "Failed to contact breached passwords site" | |
203 | msgstr "" | |
204 | ||
205 | #: flask_security/core.py:359 | |
206 | msgid "Phone number not valid e.g. missing country code" | |
207 | msgstr "" | |
208 | ||
209 | #: flask_security/core.py:360 | |
210 | msgid "Specified user does not exist" | |
211 | msgstr "此用户不存在" | |
212 | ||
213 | #: flask_security/core.py:361 | |
214 | msgid "Invalid password" | |
215 | msgstr "密码不正确" | |
216 | ||
217 | #: flask_security/core.py:362 | |
218 | msgid "Password or code submitted is not valid" | |
219 | msgstr "" | |
220 | ||
221 | #: flask_security/core.py:363 | |
222 | msgid "You have successfully logged in." | |
223 | msgstr "你已成功登录!" | |
224 | ||
225 | #: flask_security/core.py:364 | |
226 | msgid "Forgot password?" | |
227 | msgstr "忘记密码?" | |
228 | ||
229 | #: flask_security/core.py:366 | |
230 | msgid "" | |
231 | "You successfully reset your password and you have been logged in " | |
232 | "automatically." | |
233 | msgstr "你的密码已成功重置,并已自动登录。" | |
234 | ||
235 | #: flask_security/core.py:373 | |
236 | msgid "Your new password must be different than your previous password." | |
237 | msgstr "你的新密码不能与当前密码相同。" | |
238 | ||
239 | #: flask_security/core.py:376 | |
240 | msgid "You successfully changed your password." | |
241 | msgstr "你已成功更改密码!" | |
242 | ||
243 | #: flask_security/core.py:377 | |
244 | msgid "Please log in to access this page." | |
245 | msgstr "请登录访问此页面。" | |
246 | ||
247 | #: flask_security/core.py:378 | |
248 | msgid "Please reauthenticate to access this page." | |
249 | msgstr "请重新进行身份验证,以访问此页面。" | |
250 | ||
251 | #: flask_security/core.py:379 | |
252 | msgid "Reauthentication successful" | |
253 | msgstr "" | |
254 | ||
255 | #: flask_security/core.py:381 | |
256 | msgid "You can only access this endpoint when not logged in." | |
257 | msgstr "" | |
258 | ||
259 | #: flask_security/core.py:384 | |
260 | msgid "Failed to send code. Please try again later" | |
261 | msgstr "" | |
262 | ||
263 | #: flask_security/core.py:385 | |
264 | msgid "Invalid Token" | |
265 | msgstr "" | |
266 | ||
267 | #: flask_security/core.py:386 | |
268 | msgid "Your token has been confirmed" | |
269 | msgstr "" | |
270 | ||
271 | #: flask_security/core.py:388 | |
272 | msgid "You successfully changed your two-factor method." | |
273 | msgstr "" | |
274 | ||
275 | #: flask_security/core.py:392 | |
276 | msgid "You successfully confirmed password" | |
277 | msgstr "" | |
278 | ||
279 | #: flask_security/core.py:396 | |
280 | msgid "Password confirmation is needed in order to access page" | |
281 | msgstr "" | |
282 | ||
283 | #: flask_security/core.py:400 | |
284 | msgid "You currently do not have permissions to access this page" | |
285 | msgstr "" | |
286 | ||
287 | #: flask_security/core.py:403 | |
288 | msgid "Marked method is not valid" | |
289 | msgstr "" | |
290 | ||
291 | #: flask_security/core.py:405 | |
292 | msgid "You successfully disabled two factor authorization." | |
293 | msgstr "" | |
294 | ||
295 | #: flask_security/core.py:408 | |
296 | msgid "Requested method is not valid" | |
297 | msgstr "" | |
298 | ||
299 | #: flask_security/core.py:410 | |
300 | #, python-format | |
301 | msgid "Setup must be completed within %(within)s. Please start over." | |
302 | msgstr "" | |
303 | ||
304 | #: flask_security/core.py:413 | |
305 | msgid "Unified sign in setup successful" | |
306 | msgstr "" | |
307 | ||
308 | #: flask_security/core.py:414 | |
309 | msgid "You must specify a valid identity to sign in" | |
310 | msgstr "" | |
311 | ||
312 | #: flask_security/core.py:415 | |
313 | #, python-format | |
314 | msgid "Use this code to sign in: %(code)s." | |
315 | msgstr "" | |
316 | ||
317 | #: flask_security/forms.py:50 | |
318 | msgid "Email Address" | |
319 | msgstr "邮箱地址" | |
320 | ||
321 | #: flask_security/forms.py:51 | |
322 | msgid "Password" | |
323 | msgstr "密码" | |
324 | ||
325 | #: flask_security/forms.py:52 | |
326 | msgid "Remember Me" | |
327 | msgstr "记住我" | |
328 | ||
329 | #: flask_security/forms.py:53 flask_security/templates/security/_menu.html:5 | |
330 | #: flask_security/templates/security/login_user.html:6 | |
331 | #: flask_security/templates/security/send_login.html:6 | |
332 | msgid "Login" | |
333 | msgstr "登录" | |
334 | ||
335 | #: flask_security/forms.py:54 | |
336 | #: flask_security/templates/security/email/us_instructions.html:8 | |
337 | #: flask_security/templates/security/us_signin.html:6 | |
338 | msgid "Sign In" | |
339 | msgstr "" | |
340 | ||
341 | #: flask_security/forms.py:55 flask_security/templates/security/_menu.html:11 | |
342 | #: flask_security/templates/security/register_user.html:6 | |
343 | msgid "Register" | |
344 | msgstr "注册" | |
345 | ||
346 | #: flask_security/forms.py:56 | |
347 | msgid "Resend Confirmation Instructions" | |
348 | msgstr "重新发送邮件验证" | |
349 | ||
350 | #: flask_security/forms.py:57 | |
351 | msgid "Recover Password" | |
352 | msgstr "恢复密码" | |
353 | ||
354 | #: flask_security/forms.py:58 | |
355 | msgid "Reset Password" | |
356 | msgstr "重置密码" | |
357 | ||
358 | #: flask_security/forms.py:59 | |
359 | msgid "Retype Password" | |
360 | msgstr "再次确认密码" | |
361 | ||
362 | #: flask_security/forms.py:60 | |
363 | msgid "New Password" | |
364 | msgstr "新密码" | |
365 | ||
366 | #: flask_security/forms.py:61 | |
367 | msgid "Change Password" | |
368 | msgstr "更改密码" | |
369 | ||
370 | #: flask_security/forms.py:62 | |
371 | msgid "Send Login Link" | |
372 | msgstr "发送登录链接" | |
373 | ||
374 | #: flask_security/forms.py:63 | |
375 | msgid "Verify Password" | |
376 | msgstr "" | |
377 | ||
378 | #: flask_security/forms.py:64 | |
379 | msgid "Change Method" | |
380 | msgstr "" | |
381 | ||
382 | #: flask_security/forms.py:65 | |
383 | msgid "Phone Number" | |
384 | msgstr "" | |
385 | ||
386 | #: flask_security/forms.py:66 | |
387 | msgid "Authentication Code" | |
388 | msgstr "" | |
389 | ||
390 | #: flask_security/forms.py:67 | |
391 | msgid "Submit" | |
392 | msgstr "" | |
393 | ||
394 | #: flask_security/forms.py:68 | |
395 | msgid "Submit Code" | |
396 | msgstr "" | |
397 | ||
398 | #: flask_security/forms.py:69 | |
399 | msgid "Error(s)" | |
400 | msgstr "" | |
401 | ||
402 | #: flask_security/forms.py:70 | |
403 | msgid "Identity" | |
404 | msgstr "" | |
405 | ||
406 | #: flask_security/forms.py:71 | |
407 | msgid "Send Code" | |
408 | msgstr "" | |
409 | ||
410 | #: flask_security/forms.py:72 | |
411 | #, fuzzy | |
412 | msgid "Passcode" | |
413 | msgstr "密码" | |
414 | ||
415 | #: flask_security/unified_signin.py:145 | |
416 | #, fuzzy | |
417 | msgid "Code or Password" | |
418 | msgstr "恢复密码" | |
419 | ||
420 | #: flask_security/unified_signin.py:150 flask_security/unified_signin.py:270 | |
421 | msgid "Available Methods" | |
422 | msgstr "" | |
423 | ||
424 | #: flask_security/unified_signin.py:151 | |
425 | msgid "Via email" | |
426 | msgstr "" | |
427 | ||
428 | #: flask_security/unified_signin.py:151 | |
429 | msgid "Via SMS" | |
430 | msgstr "" | |
431 | ||
432 | #: flask_security/unified_signin.py:272 | |
433 | msgid "Set up using email" | |
434 | msgstr "" | |
435 | ||
436 | #: flask_security/unified_signin.py:275 | |
437 | msgid "Set up using an authenticator app (e.g. google, lastpass, authy)" | |
438 | msgstr "" | |
439 | ||
440 | #: flask_security/unified_signin.py:277 | |
441 | msgid "Set up using SMS" | |
442 | msgstr "" | |
443 | ||
444 | #: flask_security/templates/security/_menu.html:2 | |
445 | msgid "Menu" | |
446 | msgstr "菜单" | |
447 | ||
448 | #: flask_security/templates/security/_menu.html:8 | |
449 | msgid "Unified Sign In" | |
450 | msgstr "" | |
451 | ||
452 | #: flask_security/templates/security/_menu.html:14 | |
453 | msgid "Forgot password" | |
454 | msgstr "忘记密码" | |
455 | ||
456 | #: flask_security/templates/security/_menu.html:17 | |
457 | msgid "Confirm account" | |
458 | msgstr "激活账户" | |
459 | ||
460 | #: flask_security/templates/security/change_password.html:6 | |
461 | msgid "Change password" | |
462 | msgstr "更改密码" | |
463 | ||
464 | #: flask_security/templates/security/forgot_password.html:6 | |
465 | msgid "Send password reset instructions" | |
466 | msgstr "发送密码重置邮件" | |
467 | ||
468 | #: flask_security/templates/security/reset_password.html:6 | |
469 | msgid "Reset password" | |
470 | msgstr "重置密码" | |
471 | ||
472 | #: flask_security/templates/security/send_confirmation.html:6 | |
473 | msgid "Resend confirmation instructions" | |
474 | msgstr "重新发送激活邮件" | |
475 | ||
476 | #: flask_security/templates/security/two_factor_setup.html:6 | |
477 | msgid "Two-factor authentication adds an extra layer of security to your account" | |
478 | msgstr "" | |
479 | ||
480 | #: flask_security/templates/security/two_factor_setup.html:7 | |
481 | msgid "" | |
482 | "In addition to your username and password, you'll need to use a code that" | |
483 | " we will send you" | |
484 | msgstr "" | |
485 | ||
486 | #: flask_security/templates/security/two_factor_setup.html:18 | |
487 | msgid "To complete logging in, please enter the code sent to your mail" | |
488 | msgstr "" | |
489 | ||
490 | #: flask_security/templates/security/two_factor_setup.html:21 | |
491 | #: flask_security/templates/security/us_setup.html:21 | |
492 | msgid "" | |
493 | "Open your authenticator app on your device and scan the following qrcode " | |
494 | "to start receiving codes:" | |
495 | msgstr "" | |
496 | ||
497 | #: flask_security/templates/security/two_factor_setup.html:22 | |
498 | msgid "Two factor authentication code" | |
499 | msgstr "" | |
500 | ||
501 | #: flask_security/templates/security/two_factor_setup.html:25 | |
502 | msgid "To Which Phone Number Should We Send Code To?" | |
503 | msgstr "" | |
504 | ||
505 | #: flask_security/templates/security/two_factor_verify_code.html:6 | |
506 | msgid "Two-factor Authentication" | |
507 | msgstr "" | |
508 | ||
509 | #: flask_security/templates/security/two_factor_verify_code.html:7 | |
510 | msgid "Please enter your authentication code" | |
511 | msgstr "" | |
512 | ||
513 | #: flask_security/templates/security/two_factor_verify_code.html:18 | |
514 | msgid "The code for authentication was sent to your email address" | |
515 | msgstr "" | |
516 | ||
517 | #: flask_security/templates/security/two_factor_verify_code.html:21 | |
518 | msgid "A mail was sent to us in order to reset your application account" | |
519 | msgstr "" | |
520 | ||
521 | #: flask_security/templates/security/two_factor_verify_password.html:6 | |
522 | #: flask_security/templates/security/verify.html:6 | |
523 | msgid "Please Enter Your Password" | |
524 | msgstr "" | |
525 | ||
526 | #: flask_security/templates/security/us_setup.html:6 | |
527 | msgid "Setup Unified Sign In options" | |
528 | msgstr "" | |
529 | ||
530 | #: flask_security/templates/security/us_setup.html:22 | |
531 | msgid "Passwordless QRCode" | |
532 | msgstr "" | |
533 | ||
534 | #: flask_security/templates/security/us_setup.html:25 | |
535 | #: flask_security/templates/security/us_signin.html:23 | |
536 | #: flask_security/templates/security/us_verify.html:21 | |
537 | #, fuzzy | |
538 | msgid "Code has been sent" | |
539 | msgstr "你的密码已重置" | |
540 | ||
541 | #: flask_security/templates/security/us_setup.html:29 | |
542 | msgid "No methods have been enabled - nothing to setup" | |
543 | msgstr "" | |
544 | ||
545 | #: flask_security/templates/security/us_signin.html:15 | |
546 | #: flask_security/templates/security/us_verify.html:13 | |
547 | msgid "Request one-time code be sent" | |
548 | msgstr "" | |
549 | ||
550 | #: flask_security/templates/security/us_verify.html:6 | |
551 | #, fuzzy | |
552 | msgid "Please re-authenticate" | |
553 | msgstr "请重新进行身份验证,以访问此页面。" | |
554 | ||
555 | #: flask_security/templates/security/email/change_notice.html:1 | |
556 | msgid "Your password has been changed." | |
557 | msgstr "你的密码已更改。" | |
558 | ||
559 | #: flask_security/templates/security/email/change_notice.html:3 | |
560 | msgid "If you did not change your password," | |
561 | msgstr "如果你没更改你的密码," | |
562 | ||
563 | #: flask_security/templates/security/email/change_notice.html:3 | |
564 | msgid "click here to reset it" | |
565 | msgstr "点击这里重置密码" | |
566 | ||
567 | #: flask_security/templates/security/email/confirmation_instructions.html:1 | |
568 | msgid "Please confirm your email through the link below:" | |
569 | msgstr "请通过下面链接激活的你的邮箱:" | |
570 | ||
571 | #: flask_security/templates/security/email/confirmation_instructions.html:3 | |
572 | #: flask_security/templates/security/email/welcome.html:6 | |
573 | msgid "Confirm my account" | |
574 | msgstr "激活账户" | |
575 | ||
576 | #: flask_security/templates/security/email/login_instructions.html:1 | |
577 | #: flask_security/templates/security/email/welcome.html:1 | |
578 | #, python-format | |
579 | msgid "Welcome %(email)s!" | |
580 | msgstr "欢迎你,%(email)s!" | |
581 | ||
582 | #: flask_security/templates/security/email/login_instructions.html:3 | |
583 | msgid "You can log into your account through the link below:" | |
584 | msgstr "你可以通过下面链接登录的你的账户:" | |
585 | ||
586 | #: flask_security/templates/security/email/login_instructions.html:5 | |
587 | msgid "Login now" | |
588 | msgstr "立刻登录" | |
589 | ||
590 | #: flask_security/templates/security/email/reset_instructions.html:1 | |
591 | msgid "Click here to reset your password" | |
592 | msgstr "点击这里重置密码" | |
593 | ||
594 | #: flask_security/templates/security/email/two_factor_instructions.html:3 | |
595 | msgid "You can log into your account using the following code:" | |
596 | msgstr "" | |
597 | ||
598 | #: flask_security/templates/security/email/two_factor_rescue.html:1 | |
599 | msgid "can not access mail account" | |
600 | msgstr "" | |
601 | ||
602 | #: flask_security/templates/security/email/us_instructions.html:3 | |
603 | #, fuzzy | |
604 | msgid "You can sign into your account using the following code:" | |
605 | msgstr "你可以通过下面链接登录的你的账户:" | |
606 | ||
607 | #: flask_security/templates/security/email/us_instructions.html:6 | |
608 | #, fuzzy | |
609 | msgid "Or use the the link below:" | |
610 | msgstr "你可以通过下面链接激活你的邮箱:" | |
611 | ||
612 | #: flask_security/templates/security/email/welcome.html:4 | |
613 | msgid "You can confirm your email through the link below:" | |
614 | msgstr "你可以通过下面链接激活你的邮箱:" | |
615 |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.two_factor | |
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Flask-Security two_factor module | |
6 | ||
7 | :copyright: (c) 2016 by Gal Stainfeld, at Emedgene | |
8 | :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag). | |
9 | """ | |
10 | ||
11 | from flask import current_app as app, redirect, request, session | |
12 | from werkzeug.datastructures import MultiDict | |
13 | from werkzeug.local import LocalProxy | |
14 | ||
15 | from .utils import ( | |
16 | SmsSenderFactory, | |
17 | base_render_json, | |
18 | config_value, | |
19 | do_flash, | |
20 | login_user, | |
21 | json_error_response, | |
22 | url_for_security, | |
23 | ) | |
24 | from .signals import ( | |
25 | tf_code_confirmed, | |
26 | tf_disabled, | |
27 | tf_security_token_sent, | |
28 | tf_profile_changed, | |
29 | ) | |
30 | ||
31 | # Convenient references | |
32 | _security = LocalProxy(lambda: app.extensions["security"]) | |
33 | _datastore = LocalProxy(lambda: _security.datastore) | |
34 | ||
35 | ||
36 | def tf_clean_session(): | |
37 | """ | |
38 | Clean out ALL stuff stored in session (e.g. on logout) | |
39 | """ | |
40 | if config_value("TWO_FACTOR"): | |
41 | for k in [ | |
42 | "tf_state", | |
43 | "tf_user_id", | |
44 | "tf_primary_method", | |
45 | "tf_confirmed", | |
46 | "tf_remember_login", | |
47 | "tf_totp_secret", | |
48 | ]: | |
49 | session.pop(k, None) | |
50 | ||
51 | ||
52 | def tf_send_security_token(user, method, totp_secret, phone_number): | |
53 | """Sends the security token via email/sms for the specified user. | |
54 | ||
55 | :param user: The user to send the code to | |
56 | :param method: The method in which the code will be sent | |
57 | ('email' or 'sms', or 'authenticator') at the moment | |
58 | :param totp_secret: a unique shared secret of the user | |
59 | :param phone_number: If 'sms' phone number to send to | |
60 | ||
61 | There is no return value - it is assumed that exceptions are thrown by underlying | |
62 | methods that callers can catch. | |
63 | ||
64 | Flask-Security code should NOT call this directly - | |
65 | call :meth:`.UserMixin.tf_send_security_token` | |
66 | """ | |
67 | token_to_be_sent = _security._totp_factory.generate_totp_password(totp_secret) | |
68 | if method == "email" or method == "mail": | |
69 | _security._send_mail( | |
70 | config_value("EMAIL_SUBJECT_TWO_FACTOR"), | |
71 | user.email, | |
72 | "two_factor_instructions", | |
73 | user=user, | |
74 | token=token_to_be_sent, | |
75 | username=user.calc_username(), | |
76 | ) | |
77 | elif method == "sms": | |
78 | msg = "Use this code to log in: %s" % token_to_be_sent | |
79 | from_number = config_value("SMS_SERVICE_CONFIG")["PHONE_NUMBER"] | |
80 | to_number = phone_number | |
81 | sms_sender = SmsSenderFactory.createSender(config_value("SMS_SERVICE")) | |
82 | sms_sender.send_sms(from_number=from_number, to_number=to_number, msg=msg) | |
83 | ||
84 | elif method == "google_authenticator" or method == "authenticator": | |
85 | # password are generated automatically in the authenticator apps | |
86 | pass | |
87 | tf_security_token_sent.send( | |
88 | app._get_current_object(), | |
89 | user=user, | |
90 | method=method, | |
91 | token=token_to_be_sent, | |
92 | phone_number=phone_number, | |
93 | ) | |
94 | ||
95 | ||
96 | def complete_two_factor_process( | |
97 | user, primary_method, totp_secret, is_changing, remember_login=None | |
98 | ): | |
99 | """clean session according to process (login or changing two-factor method) | |
100 | and perform action accordingly | |
101 | """ | |
102 | ||
103 | _datastore.tf_set(user, primary_method, totp_secret=totp_secret) | |
104 | ||
105 | # if we are changing two-factor method | |
106 | if is_changing: | |
107 | completion_message = "TWO_FACTOR_CHANGE_METHOD_SUCCESSFUL" | |
108 | tf_profile_changed.send( | |
109 | app._get_current_object(), user=user, method=primary_method | |
110 | ) | |
111 | # if we are logging in for the first time | |
112 | else: | |
113 | completion_message = "TWO_FACTOR_LOGIN_SUCCESSFUL" | |
114 | tf_code_confirmed.send( | |
115 | app._get_current_object(), user=user, method=primary_method | |
116 | ) | |
117 | login_user(user, remember=remember_login) | |
118 | tf_clean_session() | |
119 | return completion_message | |
120 | ||
121 | ||
122 | def tf_disable(user): | |
123 | """ Disable two factor for user """ | |
124 | tf_clean_session() | |
125 | _datastore.tf_reset(user) | |
126 | tf_disabled.send(app._get_current_object(), user=user) | |
127 | ||
128 | ||
129 | def is_tf_setup(user): | |
130 | """ Return True is user account is setup for 2FA. """ | |
131 | return user.tf_totp_secret and user.tf_primary_method | |
132 | ||
133 | ||
134 | def tf_login(user, remember=None, primary_authn_via=None): | |
135 | """ Helper for two-factor authentication login | |
136 | ||
137 | This is called only when login/password have already been validated. | |
138 | This can be from login, register, confirm, unified sign in, unified magic link. | |
139 | ||
140 | The result of this is either sending a 2FA token OR starting setup for new user. | |
141 | In either case we do NOT log in user, so we must store some info in session to | |
142 | track our state (including what user). | |
143 | """ | |
144 | ||
145 | # on initial login clear any possible state out - this can happen if on same | |
146 | # machine log in more than once since for 2FA you are not authenticated | |
147 | # until complete 2FA. | |
148 | tf_clean_session() | |
149 | ||
150 | session["tf_user_id"] = user.id | |
151 | if "remember": | |
152 | session["tf_remember_login"] = remember | |
153 | ||
154 | # Set info into form for JSON response | |
155 | json_response = {"tf_required": True} | |
156 | # if user's two-factor properties are not configured | |
157 | if user.tf_primary_method is None or user.tf_totp_secret is None: | |
158 | session["tf_state"] = "setup_from_login" | |
159 | json_response["tf_state"] = "setup_from_login" | |
160 | if not _security._want_json(request): | |
161 | return redirect(url_for_security("two_factor_setup")) | |
162 | ||
163 | # if user's two-factor properties are configured | |
164 | else: | |
165 | session["tf_state"] = "ready" | |
166 | json_response["tf_state"] = "ready" | |
167 | json_response["tf_primary_method"] = user.tf_primary_method | |
168 | ||
169 | msg = user.tf_send_security_token( | |
170 | method=user.tf_primary_method, | |
171 | totp_secret=user.tf_totp_secret, | |
172 | phone_number=user.tf_phone_number, | |
173 | ) | |
174 | if msg: | |
175 | # send code didn't work | |
176 | if not _security._want_json(request): | |
177 | # This is a mess - we are deep down in the login/unified sign in flow. | |
178 | do_flash(msg, "error") | |
179 | return redirect(url_for_security("login")) | |
180 | else: | |
181 | payload = json_error_response(errors=msg) | |
182 | return _security._render_json(payload, 500, None, None) | |
183 | ||
184 | if not _security._want_json(request): | |
185 | return redirect(url_for_security("two_factor_token_validation")) | |
186 | ||
187 | # JSON response - Fake up a form - doesn't really matter which. | |
188 | form = _security.login_form(MultiDict([])) | |
189 | form.user = user | |
190 | ||
191 | return base_render_json(form, include_user=False, additional=json_response) |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.unified_signin | |
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Flask-Security Unified Signin module | |
6 | ||
7 | :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag). | |
8 | :license: MIT, see LICENSE for more details. | |
9 | ||
10 | This implements a unified sign in endpoint - allowing | |
11 | authentication via identity and passcode - where identity is configured | |
12 | via SECURITY_USER_IDENTITY_ATTRIBUTES, and allowable passcodes are either a | |
13 | password or one of US_ENABLED_METHODS. | |
14 | ||
15 | Finish up: | |
16 | - we should be able to add a phone number as part of setup even w/o any METHODS - | |
17 | i.e. to allow login with any identity (phone) and a password. | |
18 | - add username as last IDENTITY_MAPPING and allow anything...?? or just in example? | |
19 | ||
20 | Consider/Questions: | |
21 | - Allow registering/confirming with just a phone number - this likely would require | |
22 | a new register/confirm endpoint in order to implement verification. | |
23 | - Right now ChangePassword won't work - it requires an existing password - so | |
24 | if the user doesn't have one - can't change it. However ForgotPassword will in | |
25 | fact allow the user to add a password. Is that sufficient? | |
26 | - Any reason to support 'next' in form? xx?next=yyy works fine. | |
27 | - separate code validation times for SMS, email, authenticator? | |
28 | - token versus code versus passcode? Confusing terminology. | |
29 | ||
30 | """ | |
31 | ||
32 | import sys | |
33 | import time | |
34 | ||
35 | from flask import current_app as app | |
36 | from flask import abort, after_this_request, redirect, request, session | |
37 | from flask_login import current_user | |
38 | from werkzeug.datastructures import MultiDict | |
39 | from werkzeug.local import LocalProxy | |
40 | from wtforms import BooleanField, RadioField, StringField, SubmitField, validators | |
41 | ||
42 | from .confirmable import requires_confirmation | |
43 | from .decorators import anonymous_user_required, auth_required, unauth_csrf | |
44 | from .forms import Form, Required, get_form_field_label | |
45 | from .quart_compat import get_quart_status | |
46 | from .signals import us_profile_changed, us_security_token_sent | |
47 | from .twofactor import is_tf_setup, tf_login | |
48 | from .utils import ( | |
49 | _, | |
50 | SmsSenderFactory, | |
51 | base_render_json, | |
52 | check_and_get_token_status, | |
53 | config_value, | |
54 | do_flash, | |
55 | get_post_login_redirect, | |
56 | get_post_verify_redirect, | |
57 | get_message, | |
58 | get_url, | |
59 | get_within_delta, | |
60 | json_error_response, | |
61 | login_user, | |
62 | propagate_next, | |
63 | suppress_form_csrf, | |
64 | url_for_security, | |
65 | ) | |
66 | ||
67 | # Convenient references | |
68 | _security = LocalProxy(lambda: app.extensions["security"]) | |
69 | _datastore = LocalProxy(lambda: _security.datastore) | |
70 | ||
71 | ||
72 | PY3 = sys.version_info[0] == 3 | |
73 | if PY3 and get_quart_status(): # pragma: no cover | |
74 | from .async_compat import _commit # noqa: F401 | |
75 | else: | |
76 | ||
77 | def _commit(response=None): | |
78 | _datastore.commit() | |
79 | return response | |
80 | ||
81 | ||
82 | def _compute_code_methods(): | |
83 | # Return list of methods that actually send codes | |
84 | return list(set(config_value("US_ENABLED_METHODS")) - {"password", "authenticator"}) | |
85 | ||
86 | ||
87 | def _compute_setup_methods(): | |
88 | # Return list of methods that require setup | |
89 | return list(set(config_value("US_ENABLED_METHODS")) - {"password"}) | |
90 | ||
91 | ||
92 | def _compute_active_methods(user): | |
93 | # Compute methods already setup. The only oddity is that 'email' | |
94 | # can be 'auto-setup' - so include that. | |
95 | active_methods = set(config_value("US_ENABLED_METHODS")) & set( | |
96 | _datastore.us_get_totp_secrets(user).keys() | |
97 | ) | |
98 | if "email" in config_value("US_ENABLED_METHODS"): | |
99 | active_methods |= {"email"} | |
100 | return list(active_methods) | |
101 | ||
102 | ||
103 | def _us_common_validate(form): | |
104 | # Be aware - this has side effect on the form - it will fill in | |
105 | # the form.user | |
106 | ||
107 | # Validate identity - we go in order to figure out which user attribute the | |
108 | # request gave us. Note that we give up on the first 'match' even if that | |
109 | # doesn't yield a user. Why? | |
110 | for mapping in config_value("USER_IDENTITY_MAPPINGS"): | |
111 | # What we want is an ordered dict - but those don't exist for py27 - | |
112 | # so there is really just one element here. | |
113 | for ua, mapper in mapping.items(): | |
114 | # Make sure we don't validate on a column that application | |
115 | # hasn't specifically configured as a unique/identity column | |
116 | # In other words - might have a phone number for 2FA or unified | |
117 | # but don't want the user to be able to use that as primary identity | |
118 | if ua in config_value("USER_IDENTITY_ATTRIBUTES"): | |
119 | # Allow mapper to alter (coerce) to type DB requires | |
120 | idata = mapper(form.identity.data) | |
121 | if idata is not None: | |
122 | form.user = _datastore.find_user(**{ua: idata}) | |
123 | if not form.user: | |
124 | form.identity.errors.append( | |
125 | get_message("US_SPECIFY_IDENTITY")[0] | |
126 | ) | |
127 | return False | |
128 | if not form.user.is_active: | |
129 | form.identity.errors.append(get_message("DISABLED_ACCOUNT")[0]) | |
130 | return False | |
131 | return True | |
132 | return False | |
133 | ||
134 | ||
135 | class _UnifiedPassCodeForm(Form): | |
136 | """ Common form for signin and verify/reauthenticate. | |
137 | """ | |
138 | ||
139 | user = None | |
140 | authn_via = None | |
141 | ||
142 | passcode = StringField( | |
143 | get_form_field_label("passcode"), | |
144 | render_kw={"placeholder": _("Code or Password")}, | |
145 | ) | |
146 | submit = SubmitField(get_form_field_label("submit")) | |
147 | ||
148 | chosen_method = RadioField( | |
149 | _("Available Methods"), | |
150 | choices=[("email", _("Via email")), ("sms", _("Via SMS"))], | |
151 | validators=[validators.Optional()], | |
152 | ) | |
153 | submit_send_code = SubmitField(get_form_field_label("sendcode")) | |
154 | ||
155 | def __init__(self, *args, **kwargs): | |
156 | super(_UnifiedPassCodeForm, self).__init__(*args, **kwargs) | |
157 | ||
158 | def validate(self): | |
159 | if not super(_UnifiedPassCodeForm, self).validate(): | |
160 | return False | |
161 | if not self.user: | |
162 | # This is sign-in case. | |
163 | if not _us_common_validate(self): | |
164 | return False | |
165 | ||
166 | totp_secrets = _datastore.us_get_totp_secrets(self.user) | |
167 | if self.submit.data: | |
168 | # This is authn - verify passcode/password | |
169 | # Since we have a unique totp_secret for each method - we | |
170 | # can figure out which mechanism was used. | |
171 | # Note that password check requires a string (not int or None) | |
172 | passcode = self.passcode.data | |
173 | if not passcode: | |
174 | self.passcode.errors.append(get_message("INVALID_PASSWORD_CODE")[0]) | |
175 | return False | |
176 | passcode = str(passcode) | |
177 | ||
178 | ok = False | |
179 | for method in config_value("US_ENABLED_METHODS"): | |
180 | if method == "password": | |
181 | if self.user.verify_and_update_password(passcode): | |
182 | ok = True | |
183 | break | |
184 | else: | |
185 | if method in totp_secrets and _security._totp_factory.verify_totp( | |
186 | token=passcode, | |
187 | totp_secret=totp_secrets[method], | |
188 | user=self.user, | |
189 | window=config_value("US_TOKEN_VALIDITY"), | |
190 | ): | |
191 | ok = True | |
192 | break | |
193 | if not ok: | |
194 | self.passcode.errors.append(get_message("INVALID_PASSWORD_CODE")[0]) | |
195 | return False | |
196 | ||
197 | self.authn_via = method | |
198 | return True | |
199 | elif self.submit_send_code.data: | |
200 | # Send a code - chosen_method must be valid | |
201 | cm = self.chosen_method.data | |
202 | if cm not in config_value("US_ENABLED_METHODS"): | |
203 | self.chosen_method.errors.append( | |
204 | get_message("US_METHOD_NOT_AVAILABLE")[0] | |
205 | ) | |
206 | return False | |
207 | # Don't require 'email' to be setup since in the case of no password | |
208 | # we have to rely on the 'confirmation' email as verification. | |
209 | # In send_code_helper, we will setup the totp_secret for email on the fly. | |
210 | if cm != "email" and cm not in totp_secrets: | |
211 | self.chosen_method.errors.append( | |
212 | get_message("US_METHOD_NOT_AVAILABLE")[0] | |
213 | ) | |
214 | return False | |
215 | if cm == "sms" and not self.user.us_phone_number: | |
216 | # They need to us-setup! | |
217 | self.chosen_method.errors.append(get_message("PHONE_INVALID")[0]) | |
218 | return False | |
219 | return True | |
220 | return False # pragma: no cover | |
221 | ||
222 | ||
223 | class UnifiedSigninForm(_UnifiedPassCodeForm): | |
224 | """ A unified login form | |
225 | For either identity/password or request and enter code. | |
226 | """ | |
227 | ||
228 | user = None | |
229 | ||
230 | identity = StringField(get_form_field_label("identity"), validators=[Required()],) | |
231 | remember = BooleanField(get_form_field_label("remember_me")) | |
232 | ||
233 | def __init__(self, *args, **kwargs): | |
234 | super(UnifiedSigninForm, self).__init__(*args, **kwargs) | |
235 | self.remember.default = config_value("DEFAULT_REMEMBER_ME") | |
236 | ||
237 | def validate(self): | |
238 | self.user = None | |
239 | if not super(UnifiedSigninForm, self).validate(): | |
240 | return False | |
241 | ||
242 | if self.submit.data: | |
243 | # This is login | |
244 | # Only check this once authenticated to not give away info | |
245 | if requires_confirmation(self.user): | |
246 | self.identity.errors.append(get_message("CONFIRMATION_REQUIRED")[0]) | |
247 | return False | |
248 | return True | |
249 | ||
250 | ||
251 | class UnifiedVerifyForm(_UnifiedPassCodeForm): | |
252 | """ Verify authentication. | |
253 | This is for freshness 'reauthentication' required. | |
254 | """ | |
255 | ||
256 | user = None | |
257 | ||
258 | def validate(self): | |
259 | self.user = current_user | |
260 | if not super(UnifiedVerifyForm, self).validate(): | |
261 | return False | |
262 | return True | |
263 | ||
264 | ||
265 | class UnifiedSigninSetupForm(Form): | |
266 | """ Setup form """ | |
267 | ||
268 | chosen_method = RadioField( | |
269 | _("Available Methods"), | |
270 | choices=[ | |
271 | ("email", _("Set up using email")), | |
272 | ( | |
273 | "authenticator", | |
274 | _("Set up using an authenticator app (e.g. google, lastpass, authy)"), | |
275 | ), | |
276 | ("sms", _("Set up using SMS")), | |
277 | ], | |
278 | ) | |
279 | phone = StringField(get_form_field_label("phone")) | |
280 | submit = SubmitField(get_form_field_label("submit")) | |
281 | ||
282 | def __init__(self, *args, **kwargs): | |
283 | super(UnifiedSigninSetupForm, self).__init__(*args, **kwargs) | |
284 | ||
285 | def validate(self): | |
286 | if not super(UnifiedSigninSetupForm, self).validate(): | |
287 | return False | |
288 | if self.chosen_method.data not in config_value("US_ENABLED_METHODS"): | |
289 | self.chosen_method.errors.append(get_message("US_METHOD_NOT_AVAILABLE")[0]) | |
290 | return False | |
291 | ||
292 | if self.chosen_method.data == "sms": | |
293 | msg = _security._phone_util.validate_phone_number(self.phone.data) | |
294 | if msg: | |
295 | self.phone.errors.append(msg) | |
296 | return False | |
297 | ||
298 | return True | |
299 | ||
300 | ||
301 | class UnifiedSigninSetupValidateForm(Form): | |
302 | """The unified sign in setup validation form """ | |
303 | ||
304 | # These 2 filled in by view | |
305 | user = None | |
306 | totp_secret = None | |
307 | ||
308 | passcode = StringField(get_form_field_label("passcode"), validators=[Required()]) | |
309 | submit = SubmitField(get_form_field_label("submitcode")) | |
310 | ||
311 | def __init__(self, *args, **kwargs): | |
312 | super(UnifiedSigninSetupValidateForm, self).__init__(*args, **kwargs) | |
313 | ||
314 | def validate(self): | |
315 | if not super(UnifiedSigninSetupValidateForm, self).validate(): | |
316 | return False | |
317 | ||
318 | if not _security._totp_factory.verify_totp( | |
319 | token=self.passcode.data, | |
320 | totp_secret=self.totp_secret, | |
321 | user=self.user, | |
322 | window=config_value("US_TOKEN_VALIDITY"), | |
323 | ): | |
324 | self.passcode.errors.append(get_message("INVALID_PASSWORD_CODE")[0]) | |
325 | return False | |
326 | ||
327 | return True | |
328 | ||
329 | ||
330 | def _send_code_helper(form): | |
331 | # send code | |
332 | user = form.user | |
333 | method = form.chosen_method.data | |
334 | totp_secrets = _datastore.us_get_totp_secrets(user) | |
335 | # We 'auto-setup' email since in the case of no password the normal us-setup | |
336 | # mechanisms of course don't work. We rely on the fact that the user went | |
337 | # through the 'confirmation' process to validate the email. | |
338 | if method == "email" and method not in totp_secrets: | |
339 | after_this_request(_commit) | |
340 | totp_secrets[method] = _security._totp_factory.generate_totp_secret() | |
341 | _datastore.us_put_totp_secrets(user, totp_secrets) | |
342 | ||
343 | msg = user.us_send_security_token( | |
344 | method, | |
345 | totp_secret=totp_secrets[method], | |
346 | phone_number=user.us_phone_number, | |
347 | send_magic_link=True, | |
348 | ) | |
349 | code_sent = True | |
350 | if msg: | |
351 | # send code didn't work | |
352 | code_sent = False | |
353 | form.chosen_method.errors.append(msg) | |
354 | return code_sent, msg | |
355 | ||
356 | ||
357 | @anonymous_user_required | |
358 | @unauth_csrf(fall_through=True) | |
359 | def us_signin_send_code(): | |
360 | """ | |
361 | Send code view. | |
362 | This takes an identity (as configured in USER_IDENTITY_ATTRIBUTES) | |
363 | and a method request to send a code. | |
364 | """ | |
365 | form_class = _security.us_signin_form | |
366 | ||
367 | if request.is_json: | |
368 | if request.content_length: | |
369 | form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) | |
370 | else: | |
371 | form = form_class(formdata=None, meta=suppress_form_csrf()) | |
372 | else: | |
373 | form = form_class(meta=suppress_form_csrf()) | |
374 | form.submit_send_code.data = True | |
375 | ||
376 | code_methods = _compute_code_methods() | |
377 | ||
378 | if form.validate_on_submit(): | |
379 | code_sent, msg = _send_code_helper(form) | |
380 | if _security._want_json(request): | |
381 | # Not authenticated yet - so don't send any user info. | |
382 | return base_render_json( | |
383 | form, include_user=False, error_status_code=500 if msg else 400 | |
384 | ) | |
385 | ||
386 | return _security.render_template( | |
387 | config_value("US_SIGNIN_TEMPLATE"), | |
388 | us_signin_form=form, | |
389 | available_methods=config_value("US_ENABLED_METHODS"), | |
390 | code_methods=code_methods, | |
391 | chosen_method=form.chosen_method.data, | |
392 | code_sent=code_sent, | |
393 | skip_loginmenu=True, | |
394 | **_security._run_ctx_processor("us_signin") | |
395 | ) | |
396 | ||
397 | # Here on GET or failed validation | |
398 | if _security._want_json(request): | |
399 | payload = { | |
400 | "available_methods": config_value("US_ENABLED_METHODS"), | |
401 | "code_methods": code_methods, | |
402 | "identity_attributes": config_value("USER_IDENTITY_ATTRIBUTES"), | |
403 | } | |
404 | return base_render_json(form, include_user=False, additional=payload) | |
405 | ||
406 | return _security.render_template( | |
407 | config_value("US_SIGNIN_TEMPLATE"), | |
408 | us_signin_form=form, | |
409 | available_methods=config_value("US_ENABLED_METHODS"), | |
410 | code_methods=code_methods, | |
411 | skip_loginmenu=True, | |
412 | **_security._run_ctx_processor("us_signin") | |
413 | ) | |
414 | ||
415 | ||
416 | @auth_required() | |
417 | def us_verify_send_code(): | |
418 | """ | |
419 | Send code during verify. | |
420 | """ | |
421 | form_class = _security.us_verify_form | |
422 | ||
423 | if request.is_json: | |
424 | if request.content_length: | |
425 | form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) | |
426 | else: | |
427 | form = form_class(formdata=None, meta=suppress_form_csrf()) | |
428 | else: | |
429 | form = form_class(meta=suppress_form_csrf()) | |
430 | form.submit_send_code.data = True | |
431 | ||
432 | code_methods = _compute_code_methods() | |
433 | ||
434 | if form.validate_on_submit(): | |
435 | code_sent, msg = _send_code_helper(form) | |
436 | if _security._want_json(request): | |
437 | # Not authenticated yet - so don't send any user info. | |
438 | return base_render_json( | |
439 | form, include_user=False, error_status_code=500 if msg else 400 | |
440 | ) | |
441 | ||
442 | return _security.render_template( | |
443 | config_value("US_VERIFY_TEMPLATE"), | |
444 | us_verify_form=form, | |
445 | available_methods=config_value("US_ENABLED_METHODS"), | |
446 | code_methods=code_methods, | |
447 | chosen_method=form.chosen_method.data, | |
448 | code_sent=code_sent, | |
449 | skip_login_menu=True, | |
450 | send_code_to=get_url( | |
451 | _security.us_verify_send_code_url, | |
452 | qparams={"next": propagate_next(request.url)}, | |
453 | ), | |
454 | **_security._run_ctx_processor("us_verify") | |
455 | ) | |
456 | ||
457 | # Here on GET or failed validation | |
458 | if _security._want_json(request): | |
459 | payload = { | |
460 | "available_methods": config_value("US_ENABLED_METHODS"), | |
461 | "code_methods": code_methods, | |
462 | } | |
463 | return base_render_json(form, additional=payload) | |
464 | ||
465 | return _security.render_template( | |
466 | config_value("US_VERIFY_TEMPLATE"), | |
467 | us_verify_form=form, | |
468 | available_methods=config_value("US_ENABLED_METHODS"), | |
469 | code_methods=code_methods, | |
470 | skip_login_menu=True, | |
471 | send_code_to=get_url( | |
472 | _security.us_verify_send_code_url, | |
473 | qparams={"next": propagate_next(request.url)}, | |
474 | ), | |
475 | **_security._run_ctx_processor("us_verify") | |
476 | ) | |
477 | ||
478 | ||
479 | @unauth_csrf(fall_through=True) | |
480 | def us_signin(): | |
481 | """ | |
482 | Unified sign in view. | |
483 | This takes an identity (as configured in USER_IDENTITY_ATTRIBUTES) | |
484 | and a passcode (password or OTP). | |
485 | ||
486 | Allow already authenticated users. For GET this is useful for | |
487 | single-page-applications on refresh - session still active but need to | |
488 | access user info and csrf-token. | |
489 | For POST - redirects to POST_LOGIN_VIEW (forms) or returns 400 (json). | |
490 | """ | |
491 | ||
492 | if current_user.is_authenticated and request.method == "POST": | |
493 | # Just redirect current_user to POST_LOGIN_VIEW (or next). | |
494 | # While its tempting to try to logout the current user and login the | |
495 | # new requested user - that simply doesn't work with CSRF. | |
496 | ||
497 | # While this is close to anonymous_user_required - it differs in that | |
498 | # it uses get_post_login_redirect which correctly handles 'next'. | |
499 | # TODO: consider changing anonymous_user_required to also call | |
500 | # get_post_login_redirect - not sure why it never has? | |
501 | if _security._want_json(request): | |
502 | payload = json_error_response( | |
503 | errors=get_message("ANONYMOUS_USER_REQUIRED")[0] | |
504 | ) | |
505 | return _security._render_json(payload, 400, None, None) | |
506 | else: | |
507 | return redirect(get_post_login_redirect()) | |
508 | ||
509 | form_class = _security.us_signin_form | |
510 | ||
511 | if request.is_json: | |
512 | if request.content_length: | |
513 | form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) | |
514 | else: | |
515 | form = form_class(formdata=None, meta=suppress_form_csrf()) | |
516 | else: | |
517 | form = form_class(meta=suppress_form_csrf()) | |
518 | form.submit.data = True | |
519 | ||
520 | if form.validate_on_submit(): | |
521 | # Require multi-factor is it is enabled, and the method | |
522 | # we authenticated with requires it and either user has requested MFA or it is | |
523 | # required. | |
524 | remember_me = form.remember.data if "remember" in form else None | |
525 | if ( | |
526 | config_value("TWO_FACTOR") | |
527 | and form.authn_via in config_value("US_MFA_REQUIRED") | |
528 | and (config_value("TWO_FACTOR_REQUIRED") or is_tf_setup(form.user)) | |
529 | ): | |
530 | return tf_login( | |
531 | form.user, remember=remember_me, primary_authn_via=form.authn_via | |
532 | ) | |
533 | ||
534 | after_this_request(_commit) | |
535 | login_user(form.user, remember=remember_me, authn_via=[form.authn_via]) | |
536 | ||
537 | if _security._want_json(request): | |
538 | return base_render_json(form, include_auth_token=True) | |
539 | ||
540 | return redirect(get_post_login_redirect()) | |
541 | ||
542 | # Here on GET or failed POST validate | |
543 | code_methods = _compute_code_methods() | |
544 | if _security._want_json(request): | |
545 | payload = { | |
546 | "available_methods": config_value("US_ENABLED_METHODS"), | |
547 | "code_methods": code_methods, | |
548 | "identity_attributes": config_value("USER_IDENTITY_ATTRIBUTES"), | |
549 | } | |
550 | return base_render_json(form, include_user=False, additional=payload) | |
551 | ||
552 | if current_user.is_authenticated: | |
553 | # Basically a no-op if authenticated - just perform the same | |
554 | # post-login redirect as if user just logged in. | |
555 | return redirect(get_post_login_redirect()) | |
556 | ||
557 | # On error - wipe code | |
558 | form.passcode.data = None | |
559 | return _security.render_template( | |
560 | config_value("US_SIGNIN_TEMPLATE"), | |
561 | us_signin_form=form, | |
562 | available_methods=config_value("US_ENABLED_METHODS"), | |
563 | code_methods=code_methods, | |
564 | skip_login_menu=True, | |
565 | **_security._run_ctx_processor("us_signin") | |
566 | ) | |
567 | ||
568 | ||
569 | @auth_required() | |
570 | def us_verify(): | |
571 | """ | |
572 | Re-authenticate to reset freshness time. | |
573 | This is likely the result of a reauthn_handler redirect, which | |
574 | will have filled in ?next=xxx - which we want to carefully not lose as we | |
575 | go through these steps. | |
576 | """ | |
577 | form_class = _security.us_verify_form | |
578 | ||
579 | if request.is_json: | |
580 | if request.content_length: | |
581 | form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) | |
582 | else: | |
583 | form = form_class(formdata=None, meta=suppress_form_csrf()) | |
584 | else: | |
585 | form = form_class(meta=suppress_form_csrf()) | |
586 | form.submit.data = True | |
587 | ||
588 | code_methods = _compute_code_methods() | |
589 | ||
590 | if form.validate_on_submit(): | |
591 | # verified - so set freshness time. | |
592 | session["fs_paa"] = time.time() | |
593 | ||
594 | if _security._want_json(request): | |
595 | return base_render_json(form, include_auth_token=True) | |
596 | ||
597 | do_flash(*get_message("REAUTHENTICATION_SUCCESSFUL")) | |
598 | return redirect(get_post_verify_redirect()) | |
599 | ||
600 | # Here on GET or failed POST validate | |
601 | if _security._want_json(request): | |
602 | payload = { | |
603 | "available_methods": config_value("US_ENABLED_METHODS"), | |
604 | "code_methods": code_methods, | |
605 | } | |
606 | return base_render_json(form, additional=payload) | |
607 | ||
608 | # On error - wipe code | |
609 | form.passcode.data = None | |
610 | return _security.render_template( | |
611 | config_value("US_VERIFY_TEMPLATE"), | |
612 | us_verify_form=form, | |
613 | code_methods=code_methods, | |
614 | skip_login_menu=True, | |
615 | send_code_to=get_url( | |
616 | _security.us_verify_send_code_url, | |
617 | qparams={"next": propagate_next(request.url)}, | |
618 | ), | |
619 | **_security._run_ctx_processor("us_verify") | |
620 | ) | |
621 | ||
622 | ||
623 | @anonymous_user_required | |
624 | def us_verify_link(): | |
625 | """ | |
626 | Used to verify a magic email link. GET only | |
627 | """ | |
628 | if not all(v in request.args for v in ["email", "code"]): | |
629 | m, c = get_message("API_ERROR") | |
630 | if _security.redirect_behavior == "spa": | |
631 | return redirect(get_url(_security.login_error_view, qparams={c: m})) | |
632 | do_flash(m, c) | |
633 | return redirect(url_for_security("us_signin")) | |
634 | ||
635 | user = _datastore.find_user(email=request.args.get("email")) | |
636 | if not user or not user.active: | |
637 | if not user: | |
638 | m, c = get_message("USER_DOES_NOT_EXIST") | |
639 | else: | |
640 | m, c = get_message("DISABLED_ACCOUNT") | |
641 | if _security.redirect_behavior == "spa": | |
642 | return redirect(get_url(_security.login_error_view, qparams={c: m})) | |
643 | do_flash(m, c) | |
644 | return redirect(url_for_security("us_signin")) | |
645 | ||
646 | totp_secrets = _datastore.us_get_totp_secrets(user) | |
647 | if "email" not in totp_secrets or not _security._totp_factory.verify_totp( | |
648 | token=request.args.get("code"), | |
649 | totp_secret=totp_secrets["email"], | |
650 | user=user, | |
651 | window=config_value("US_TOKEN_VALIDITY"), | |
652 | ): | |
653 | m, c = get_message("INVALID_CODE") | |
654 | if _security.redirect_behavior == "spa": | |
655 | return redirect( | |
656 | get_url( | |
657 | _security.login_error_view, | |
658 | qparams=user.get_redirect_qparams({c: m}), | |
659 | ) | |
660 | ) | |
661 | do_flash(m, c) | |
662 | return redirect(url_for_security("us_signin")) | |
663 | ||
664 | if ( | |
665 | config_value("TWO_FACTOR") | |
666 | and "email" in config_value("US_MFA_REQUIRED") | |
667 | and (config_value("TWO_FACTOR_REQUIRED") or is_tf_setup(user)) | |
668 | ): | |
669 | # tf_login doesn't know anything about "spa" etc. In general two-factor | |
670 | # isn't quite ready for SPA. So we return an error via a redirect rather | |
671 | # than mess up SPA applications. To be clear - this simply doesn't | |
672 | # work - using a magic link w/ 2FA - need to use code. | |
673 | if _security.redirect_behavior == "spa": | |
674 | return redirect( | |
675 | get_url( | |
676 | _security.login_error_view, | |
677 | qparams=user.get_redirect_qparams({"tf_required": 1}), | |
678 | ) | |
679 | ) | |
680 | return tf_login(user, primary_authn_via="email") | |
681 | ||
682 | login_user(user, authn_via=["email"]) | |
683 | after_this_request(_commit) | |
684 | if _security.redirect_behavior == "spa": | |
685 | # We do NOT send the authentication token here since the only way to | |
686 | # send it would be via a query param and that isn't secure. (logging and | |
687 | # possibly HTTP Referer header). | |
688 | # This means that this can only work if sessions are active which sort of | |
689 | # makes sense - otherwise you need to use /us-signin with a code. | |
690 | return redirect( | |
691 | get_url(_security.post_login_view, qparams=user.get_redirect_qparams()) | |
692 | ) | |
693 | ||
694 | do_flash(*get_message("PASSWORDLESS_LOGIN_SUCCESSFUL")) | |
695 | return redirect(get_post_login_redirect()) | |
696 | ||
697 | ||
698 | @auth_required( | |
699 | within=lambda: config_value("FRESHNESS"), | |
700 | grace=lambda: config_value("FRESHNESS_GRACE_PERIOD"), | |
701 | ) | |
702 | def us_setup(): | |
703 | """ | |
704 | Change unified sign in methods. | |
705 | We want to verify the new method - so don't store anything yet in DB | |
706 | use a timed signed token to pass along state. | |
707 | GET - retrieve current info (json) or form. | |
708 | """ | |
709 | form_class = _security.us_setup_form | |
710 | ||
711 | if request.is_json: | |
712 | if request.content_length: | |
713 | form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) | |
714 | else: | |
715 | form = form_class(formdata=None, meta=suppress_form_csrf()) | |
716 | else: | |
717 | form = form_class(meta=suppress_form_csrf()) | |
718 | ||
719 | setup_methods = _compute_setup_methods() | |
720 | active_methods = _compute_active_methods(current_user) | |
721 | ||
722 | if form.validate_on_submit(): | |
723 | method = form.chosen_method.data | |
724 | # Always generate a totp_secret. We don't set it in the DB until | |
725 | # user has successfully validated. | |
726 | totp = _security._totp_factory.generate_totp_secret() | |
727 | ||
728 | # N.B. totp (totp_secret) is actually encrypted - so it seems safe enough | |
729 | # to send it to the user. | |
730 | # Only check phone number if SMS (see form validate) | |
731 | state = { | |
732 | "totp_secret": totp, | |
733 | "chosen_method": method, | |
734 | "phone_number": _security._phone_util.get_canonical_form(form.phone.data) | |
735 | if method == "sms" | |
736 | else None, | |
737 | } | |
738 | msg = current_user.us_send_security_token( | |
739 | method=method, | |
740 | totp_secret=state["totp_secret"], | |
741 | phone_number=state["phone_number"], | |
742 | ) | |
743 | if msg: | |
744 | # sending didn't work. | |
745 | form.chosen_method.errors.append(msg) | |
746 | if _security._want_json(request): | |
747 | # Not authenticated yet - so don't send any user info. | |
748 | return base_render_json( | |
749 | form, include_user=False, error_status_code=500 if msg else 400 | |
750 | ) | |
751 | return _security.render_template( | |
752 | config_value("US_SETUP_TEMPLATE"), | |
753 | available_methods=config_value("US_ENABLED_METHODS"), | |
754 | active_methods=active_methods, | |
755 | setup_methods=setup_methods, | |
756 | us_setup_form=form, | |
757 | **_security._run_ctx_processor("us_setup") | |
758 | ) | |
759 | ||
760 | state_token = _security.us_setup_serializer.dumps(state) | |
761 | ||
762 | if _security._want_json(request): | |
763 | payload = {"state": state_token, "chosen_method": form.chosen_method.data} | |
764 | return base_render_json(form, include_user=False, additional=payload) | |
765 | return _security.render_template( | |
766 | config_value("US_SETUP_TEMPLATE"), | |
767 | available_methods=config_value("US_ENABLED_METHODS"), | |
768 | active_methods=active_methods, | |
769 | setup_methods=setup_methods, | |
770 | code_sent=form.chosen_method.data in _compute_code_methods(), | |
771 | chosen_method=form.chosen_method.data, | |
772 | us_setup_form=form, | |
773 | us_setup_validate_form=_security.us_setup_validate_form(), | |
774 | state=state_token, | |
775 | **_security._run_ctx_processor("us_setup") | |
776 | ) | |
777 | ||
778 | # Get here on initial new setup (GET) | |
779 | # Or failure of POST | |
780 | if _security._want_json(request): | |
781 | payload = { | |
782 | "identity_attributes": config_value("USER_IDENTITY_ATTRIBUTES"), | |
783 | "available_methods": config_value("US_ENABLED_METHODS"), | |
784 | "active_methods": active_methods, | |
785 | "setup_methods": setup_methods, | |
786 | "phone": current_user.us_phone_number, | |
787 | } | |
788 | return base_render_json(form, include_user=False, additional=payload) | |
789 | ||
790 | # Show user existing phone number | |
791 | form.phone.data = current_user.us_phone_number | |
792 | return _security.render_template( | |
793 | config_value("US_SETUP_TEMPLATE"), | |
794 | available_methods=config_value("US_ENABLED_METHODS"), | |
795 | active_methods=active_methods, | |
796 | setup_methods=setup_methods, | |
797 | us_setup_form=form, | |
798 | **_security._run_ctx_processor("us_setup") | |
799 | ) | |
800 | ||
801 | ||
802 | @auth_required() | |
803 | def us_setup_validate(token): | |
804 | """ | |
805 | Validate new setup. | |
806 | The token is the state variable which is signed and timed | |
807 | and contains all the state that once confirmed will be stored in the user record. | |
808 | """ | |
809 | ||
810 | form_class = _security.us_setup_validate_form | |
811 | ||
812 | if request.is_json: | |
813 | form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) | |
814 | else: | |
815 | form = form_class(meta=suppress_form_csrf()) | |
816 | ||
817 | expired, invalid, state = check_and_get_token_status( | |
818 | token, "us_setup", get_within_delta("US_SETUP_WITHIN") | |
819 | ) | |
820 | if invalid: | |
821 | m, c = get_message("API_ERROR") | |
822 | if expired: | |
823 | m, c = get_message("US_SETUP_EXPIRED", within=config_value("US_SETUP_WITHIN")) | |
824 | if invalid or expired: | |
825 | if _security._want_json(request): | |
826 | payload = json_error_response(errors=m) | |
827 | return _security._render_json(payload, 400, None, None) | |
828 | do_flash(m, c) | |
829 | return redirect(url_for_security("us_setup")) | |
830 | ||
831 | form.totp_secret = state["totp_secret"] | |
832 | form.user = current_user | |
833 | ||
834 | if form.validate_on_submit(): | |
835 | after_this_request(_commit) | |
836 | method = state["chosen_method"] | |
837 | phone = state["phone_number"] if method == "sms" else None | |
838 | _datastore.us_set(current_user, method, state["totp_secret"], phone) | |
839 | ||
840 | us_profile_changed.send( | |
841 | app._get_current_object(), user=current_user, method=method | |
842 | ) | |
843 | if _security._want_json(request): | |
844 | return base_render_json( | |
845 | form, | |
846 | include_user=False, | |
847 | additional=dict( | |
848 | chosen_method=method, phone=current_user.us_phone_number | |
849 | ), | |
850 | ) | |
851 | else: | |
852 | do_flash(*get_message("US_SETUP_SUCCESSFUL")) | |
853 | return redirect( | |
854 | get_url(_security.us_post_setup_view) | |
855 | or get_url(_security.post_login_view) | |
856 | ) | |
857 | ||
858 | # Code not correct/outdated. | |
859 | if _security._want_json(request): | |
860 | return base_render_json(form, include_user=False) | |
861 | m, c = get_message("INVALID_PASSWORD_CODE") | |
862 | do_flash(m, c) | |
863 | return redirect(url_for_security("us_setup")) | |
864 | ||
865 | ||
866 | @auth_required() | |
867 | def us_qrcode(token): | |
868 | ||
869 | if "authenticator" not in config_value("US_ENABLED_METHODS"): | |
870 | return abort(404) | |
871 | expired, invalid, state = check_and_get_token_status( | |
872 | token, "us_setup", get_within_delta("US_SETUP_WITHIN") | |
873 | ) | |
874 | if expired or invalid: | |
875 | return abort(400) | |
876 | ||
877 | try: | |
878 | import pyqrcode | |
879 | ||
880 | # By convention, the URI should have the username that the user | |
881 | # logs in with. | |
882 | username = current_user.calc_username() | |
883 | url = pyqrcode.create( | |
884 | _security._totp_factory.get_totp_uri( | |
885 | username if username else "Unknown", state["totp_secret"] | |
886 | ) | |
887 | ) | |
888 | except ImportError: # pragma: no cover | |
889 | raise | |
890 | from io import BytesIO | |
891 | ||
892 | stream = BytesIO() | |
893 | url.svg(stream, scale=3) | |
894 | return ( | |
895 | stream.getvalue(), | |
896 | 200, | |
897 | { | |
898 | "Content-Type": "image/svg+xml", | |
899 | "Cache-Control": "no-cache, no-store, must-revalidate", | |
900 | "Pragma": "no-cache", | |
901 | "Expires": "0", | |
902 | }, | |
903 | ) | |
904 | ||
905 | ||
906 | def us_send_security_token( | |
907 | user, method, totp_secret, phone_number, send_magic_link=False | |
908 | ): | |
909 | """ Generate and send the security code. | |
910 | ||
911 | :param user: The user to send the code to | |
912 | :param method: The method in which the code will be sent | |
913 | :param totp_secret: the unique shared secret of the user | |
914 | :param phone_number: If 'sms' phone number to send to | |
915 | :param send_magic_link: If true a magic link that can be clicked on will be sent. | |
916 | This shouldn't be sent during a setup. | |
917 | ||
918 | There is no return value - it is assumed that exceptions are thrown by underlying | |
919 | methods that callers can catch. | |
920 | ||
921 | Flask-Security code should NOT call this directly - | |
922 | call :meth:`.UserMixin.us_send_security_token` | |
923 | ||
924 | .. versionadded:: 3.4.0 | |
925 | """ | |
926 | token = _security._totp_factory.generate_totp_password(totp_secret) | |
927 | ||
928 | if method == "email": | |
929 | login_link = None | |
930 | if send_magic_link: | |
931 | login_link = url_for_security( | |
932 | "us_verify_link", email=user.email, code=token, _external=True | |
933 | ) | |
934 | _security._send_mail( | |
935 | config_value("US_EMAIL_SUBJECT"), | |
936 | user.email, | |
937 | "us_instructions", | |
938 | user=user, | |
939 | username=user.calc_username(), | |
940 | token=token, | |
941 | login_link=login_link, | |
942 | ) | |
943 | elif method == "sms": | |
944 | m, c = get_message("USE_CODE", code=token) | |
945 | from_number = config_value("SMS_SERVICE_CONFIG")["PHONE_NUMBER"] | |
946 | to_number = phone_number | |
947 | sms_sender = SmsSenderFactory.createSender(config_value("SMS_SERVICE")) | |
948 | sms_sender.send_sms(from_number=from_number, to_number=to_number, msg=m) | |
949 | ||
950 | elif method == "authenticator" or method == "password": | |
951 | # tokens are generated automatically with authenticator apps | |
952 | # and passwords are well passwords | |
953 | # Still go ahead and notify signal receivers that they requested it. | |
954 | token = None | |
955 | us_security_token_sent.send( | |
956 | app._get_current_object(), | |
957 | user=user, | |
958 | method=method, | |
959 | token=token, | |
960 | phone_number=phone_number, | |
961 | send_magic_link=send_magic_link, | |
962 | ) |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.utils | |
3 | ~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Flask-Security utils module | |
6 | ||
7 | :copyright: (c) 2012-2019 by Matt Wright. | |
8 | :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag). | |
9 | :license: MIT, see LICENSE for more details. | |
10 | """ | |
11 | import abc | |
12 | import base64 | |
13 | import datetime | |
14 | from functools import partial | |
15 | import hashlib | |
16 | import hmac | |
17 | import sys | |
18 | import time | |
19 | import warnings | |
20 | from contextlib import contextmanager | |
21 | from datetime import timedelta | |
22 | ||
23 | from flask import _request_ctx_stack, current_app, flash, g, request, session, url_for | |
24 | from flask.json import JSONEncoder | |
25 | from flask.signals import message_flashed | |
26 | from flask_login import login_user as _login_user | |
27 | from flask_login import logout_user as _logout_user | |
28 | from flask_login import current_user | |
29 | from flask_login import COOKIE_NAME as REMEMBER_COOKIE_NAME | |
30 | from flask_mail import Message | |
31 | from flask_principal import AnonymousIdentity, Identity, identity_changed, Need | |
32 | from flask_wtf import csrf | |
33 | from wtforms import validators, ValidationError | |
34 | from itsdangerous import BadSignature, SignatureExpired | |
35 | from speaklater import is_lazy_string | |
36 | from werkzeug.local import LocalProxy | |
37 | from werkzeug.datastructures import MultiDict | |
38 | from .quart_compat import best | |
39 | from .signals import ( | |
40 | login_instructions_sent, | |
41 | reset_password_instructions_sent, | |
42 | user_authenticated, | |
43 | user_registered, | |
44 | ) | |
45 | ||
46 | try: # pragma: no cover | |
47 | from urlparse import parse_qsl, parse_qs, urlsplit, urlunsplit | |
48 | from urllib import urlencode | |
49 | except ImportError: # pragma: no cover | |
50 | from urllib.parse import parse_qsl, parse_qs, urlsplit, urlunsplit, urlencode | |
51 | ||
52 | # Convenient references | |
53 | _security = LocalProxy(lambda: current_app.extensions["security"]) | |
54 | ||
55 | _datastore = LocalProxy(lambda: _security.datastore) | |
56 | ||
57 | _pwd_context = LocalProxy(lambda: _security.pwd_context) | |
58 | ||
59 | _hashing_context = LocalProxy(lambda: _security.hashing_context) | |
60 | ||
61 | localize_callback = LocalProxy(lambda: _security.i18n_domain.gettext) | |
62 | ||
63 | PY3 = sys.version_info[0] == 3 | |
64 | ||
65 | if PY3: # pragma: no cover | |
66 | string_types = (str,) # noqa | |
67 | text_type = str # noqa | |
68 | else: # pragma: no cover | |
69 | string_types = (basestring,) # noqa | |
70 | text_type = unicode # noqa | |
71 | ||
72 | FsPermNeed = partial(Need, "fsperm") | |
73 | FsPermNeed.__doc__ = """A need with the method preset to `"fsperm"`.""" | |
74 | ||
75 | ||
76 | def _(translate): | |
77 | """Identity function to mark strings for translation.""" | |
78 | return translate | |
79 | ||
80 | ||
81 | def find_csrf_field_name(): | |
82 | """ | |
83 | We need to clear it on logout (since that isn't being done by Flask-WTF). | |
84 | The field name is configurable withing Flask-WTF as well as being | |
85 | overridable. | |
86 | We take the field name from the login_form as set by the configuration. | |
87 | """ | |
88 | form = _security.login_form(MultiDict([])) | |
89 | if hasattr(form.meta, "csrf_field_name"): | |
90 | return form.meta.csrf_field_name | |
91 | return None | |
92 | ||
93 | ||
94 | def login_user(user, remember=None, authn_via=None): | |
95 | """Perform the login routine. | |
96 | ||
97 | If *SECURITY_TRACKABLE* is used, make sure you commit changes after this | |
98 | request (i.e. ``app.security.datastore.commit()``). | |
99 | ||
100 | :param user: The user to login | |
101 | :param remember: Flag specifying if the remember cookie should be set. | |
102 | Defaults to ``False`` | |
103 | :param authn_via: A list of strings denoting which mechanism(s) the user | |
104 | authenticated with. | |
105 | These should be one or more of ["password", "sms", "authenticator", "email"] or | |
106 | other 'auto-login' mechanisms. | |
107 | """ | |
108 | ||
109 | if remember is None: | |
110 | remember = config_value("DEFAULT_REMEMBER_ME") | |
111 | ||
112 | if not _login_user(user, remember): # pragma: no cover | |
113 | return False | |
114 | ||
115 | if _security.trackable: | |
116 | remote_addr = request.remote_addr or None # make sure it is None | |
117 | ||
118 | old_current_login, new_current_login = ( | |
119 | user.current_login_at, | |
120 | _security.datetime_factory(), | |
121 | ) | |
122 | old_current_ip, new_current_ip = user.current_login_ip, remote_addr | |
123 | ||
124 | user.last_login_at = old_current_login or new_current_login | |
125 | user.current_login_at = new_current_login | |
126 | user.last_login_ip = old_current_ip | |
127 | user.current_login_ip = new_current_ip | |
128 | user.login_count = user.login_count + 1 if user.login_count else 1 | |
129 | ||
130 | _datastore.put(user) | |
131 | ||
132 | session["fs_cc"] = "set" # CSRF cookie | |
133 | session["fs_paa"] = time.time() # Primary authentication at - timestamp | |
134 | ||
135 | identity_changed.send(current_app._get_current_object(), identity=Identity(user.id)) | |
136 | ||
137 | user_authenticated.send( | |
138 | current_app._get_current_object(), user=user, authn_via=authn_via | |
139 | ) | |
140 | return True | |
141 | ||
142 | ||
143 | def logout_user(): | |
144 | """Logs out the current user. | |
145 | ||
146 | This will also clean up the remember me cookie if it exists. | |
147 | ||
148 | This sends an ``identity_changed`` signal to note that the current | |
149 | identity is now the `AnonymousIdentity` | |
150 | """ | |
151 | ||
152 | for key in ("identity.name", "identity.auth_type", "fs_paa", "fs_gexp"): | |
153 | session.pop(key, None) | |
154 | ||
155 | # Clear csrf token between sessions. | |
156 | # Ideally this would be handled by Flask-WTF but... | |
157 | # We don't clear entire session since Flask-Login seems to like having it. | |
158 | csrf_field_name = find_csrf_field_name() | |
159 | if csrf_field_name: | |
160 | session.pop(csrf_field_name, None) | |
161 | # Flask-WTF 'caches' csrf_token - and only set the session if not already | |
162 | # in 'g'. Be sure to clear both. This affects at least /confirm | |
163 | g.pop(csrf_field_name, None) | |
164 | session["fs_cc"] = "clear" | |
165 | identity_changed.send( | |
166 | current_app._get_current_object(), identity=AnonymousIdentity() | |
167 | ) | |
168 | _logout_user() | |
169 | ||
170 | ||
171 | def _py2timestamp(dt): | |
172 | return time.mktime(dt.timetuple()) + dt.microsecond / 1e6 | |
173 | ||
174 | ||
175 | def check_and_update_authn_fresh(within, grace): | |
176 | """ Check if user authenticated within specified time and update grace period. | |
177 | ||
178 | :param within: A timedelta specifying the maximum time in the past that the caller | |
179 | authenticated that is still considered 'fresh'. | |
180 | :param grace: A timedelta that, if the current session is considered 'fresh' | |
181 | will set a grace period for which freshness won't be checked. | |
182 | The intent here is that the caller shouldn't get part-way though | |
183 | a set of operations and suddenly be required to authenticate again. | |
184 | ||
185 | If within.total_seconds() is negative, will always return True (always 'fresh'). | |
186 | This effectively just disables this entire mechanism. | |
187 | ||
188 | If "fs_gexp" is in the session and the current timestamp is less than that, | |
189 | return True and extend grace time (i.e. set fs_gexp to current time + grace). | |
190 | ||
191 | If not within the grace period, and within.total_seconds() is 0, | |
192 | return False (not fresh). | |
193 | ||
194 | Be aware that for this to work, sessions and therefore session cookies | |
195 | must be functioning and being sent as part of the request. | |
196 | ||
197 | .. warning:: | |
198 | Be sure the caller is already authenticated PRIOR to calling this method. | |
199 | ||
200 | .. versionadded:: 3.4.0 | |
201 | """ | |
202 | ||
203 | if within.total_seconds() < 0: | |
204 | # this means 'always fresh' | |
205 | return True | |
206 | ||
207 | if "fs_paa" not in session: | |
208 | # No session, you can't play. | |
209 | return False | |
210 | ||
211 | now = datetime.datetime.utcnow() | |
212 | new_exp = now + grace | |
213 | # grace_ts = int(new_exp.timestamp()) | |
214 | grace_ts = int(_py2timestamp(new_exp)) | |
215 | ||
216 | fs_gexp = session.get("fs_gexp", None) | |
217 | if fs_gexp: | |
218 | # if now.timestamp() < fs_gexp: | |
219 | if _py2timestamp(now) < fs_gexp: | |
220 | # Within grace period - extend it and we're good. | |
221 | session["fs_gexp"] = grace_ts | |
222 | return True | |
223 | ||
224 | # Special case 0 - return False always, but set grace period. | |
225 | if within.total_seconds() == 0: | |
226 | session["fs_gexp"] = grace_ts | |
227 | return False | |
228 | ||
229 | authn_time = datetime.datetime.utcfromtimestamp(session["fs_paa"]) | |
230 | # allow for some time drift where it's possible authn_time is in the future | |
231 | # but lets be cautious and not allow arbitrary future times | |
232 | delta = now - authn_time | |
233 | if within > delta > -within: | |
234 | session["fs_gexp"] = grace_ts | |
235 | return True | |
236 | return False | |
237 | ||
238 | ||
239 | def get_hmac(password): | |
240 | """Returns a Base64 encoded HMAC+SHA512 of the password signed with | |
241 | the salt specified by *SECURITY_PASSWORD_SALT*. | |
242 | ||
243 | :param password: The password to sign | |
244 | """ | |
245 | salt = _security.password_salt | |
246 | ||
247 | if salt is None: | |
248 | raise RuntimeError( | |
249 | "The configuration value `SECURITY_PASSWORD_SALT` must " | |
250 | "not be None when the value of `SECURITY_PASSWORD_HASH` is " | |
251 | 'set to "%s"' % _security.password_hash | |
252 | ) | |
253 | ||
254 | h = hmac.new(encode_string(salt), encode_string(password), hashlib.sha512) | |
255 | return base64.b64encode(h.digest()) | |
256 | ||
257 | ||
258 | def verify_password(password, password_hash): | |
259 | """Returns ``True`` if the password matches the supplied hash. | |
260 | ||
261 | :param password: A plaintext password to verify | |
262 | :param password_hash: The expected hash value of the password | |
263 | (usually from your database) | |
264 | """ | |
265 | if use_double_hash(password_hash): | |
266 | password = get_hmac(password) | |
267 | ||
268 | return _pwd_context.verify(password, password_hash) | |
269 | ||
270 | ||
271 | def verify_and_update_password(password, user): | |
272 | """Returns ``True`` if the password is valid for the specified user. | |
273 | ||
274 | Additionally, the hashed password in the database is updated if the | |
275 | hashing algorithm happens to have changed. | |
276 | ||
277 | N.B. you MUST call DB commit if you are using a session-based datastore | |
278 | (such as SqlAlchemy) since the user instance might have been altered | |
279 | (i.e. ``app.security.datastore.commit()``). | |
280 | This is usually handled in the view. | |
281 | ||
282 | :param password: A plaintext password to verify | |
283 | :param user: The user to verify against | |
284 | ||
285 | .. tip:: | |
286 | This should not be called directly - rather use | |
287 | :meth:`.UserMixin.verify_and_update_password` | |
288 | ||
289 | """ | |
290 | if use_double_hash(user.password): | |
291 | verified = _pwd_context.verify(get_hmac(password), user.password) | |
292 | else: | |
293 | # Try with original password. | |
294 | verified = _pwd_context.verify(password, user.password) | |
295 | ||
296 | if verified and _pwd_context.needs_update(user.password): | |
297 | user.password = hash_password(password) | |
298 | _datastore.put(user) | |
299 | return verified | |
300 | ||
301 | ||
302 | def encrypt_password(password): # pragma: no cover | |
303 | """Encrypt the specified plaintext password. | |
304 | ||
305 | It uses the configured encryption options. | |
306 | ||
307 | .. deprecated:: 2.0.2 | |
308 | Use :func:`hash_password` instead. | |
309 | ||
310 | :param password: The plaintext password to encrypt | |
311 | """ | |
312 | warnings.warn( | |
313 | "Please use hash_password instead of encrypt_password.", DeprecationWarning | |
314 | ) | |
315 | return hash_password(password) | |
316 | ||
317 | ||
318 | def hash_password(password): | |
319 | """Hash the specified plaintext password. | |
320 | ||
321 | Unless the hash algorithm (as specified by `SECURITY_PASSWORD_HASH`) is listed in | |
322 | the configuration variable `SECURITY_PASSWORD_SINGLE_HASH`, | |
323 | perform a double hash - first create an HMAC from the plaintext password | |
324 | and the value of `SECURITY_PASSWORD_SALT`, | |
325 | then use the configured hashing algorithm. | |
326 | This satisfies OWASP/ASVS section 2.4.5: 'provide additional | |
327 | iteration of a key derivation'. | |
328 | ||
329 | .. versionadded:: 2.0.2 | |
330 | ||
331 | :param password: The plaintext password to hash | |
332 | """ | |
333 | if use_double_hash(): | |
334 | password = get_hmac(password).decode("ascii") | |
335 | ||
336 | # Passing in options as part of hash is deprecated in passlib 1.7 | |
337 | # and new algorithms like argon2 don't even support it. | |
338 | return _pwd_context.hash( | |
339 | password, | |
340 | **config_value("PASSWORD_HASH_OPTIONS", default={}).get( | |
341 | _security.password_hash, {} | |
342 | ) | |
343 | ) | |
344 | ||
345 | ||
346 | def encode_string(string): | |
347 | """Encodes a string to bytes, if it isn't already. | |
348 | ||
349 | :param string: The string to encode""" | |
350 | ||
351 | if isinstance(string, text_type): | |
352 | string = string.encode("utf-8") | |
353 | return string | |
354 | ||
355 | ||
356 | def hash_data(data): | |
357 | return _hashing_context.hash(encode_string(data)) | |
358 | ||
359 | ||
360 | def verify_hash(hashed_data, compare_data): | |
361 | return _hashing_context.verify(encode_string(compare_data), hashed_data) | |
362 | ||
363 | ||
364 | def suppress_form_csrf(): | |
365 | """ | |
366 | Return meta contents if we should suppress form from attempting to validate CSRF. | |
367 | ||
368 | If app doesn't want CSRF for unauth endpoints then check if caller is authenticated | |
369 | or not (many endpoints can be called either way). | |
370 | """ | |
371 | ctx = _request_ctx_stack.top | |
372 | if hasattr(ctx, "fs_ignore_csrf") and ctx.fs_ignore_csrf: | |
373 | # This is the case where CsrfProtect was already called (e.g. @auth_required) | |
374 | return {"csrf": False} | |
375 | if ( | |
376 | config_value("CSRF_IGNORE_UNAUTH_ENDPOINTS") | |
377 | and not current_user.is_authenticated | |
378 | ): | |
379 | return {"csrf": False} | |
380 | return {} | |
381 | ||
382 | ||
383 | def do_flash(message, category=None): | |
384 | """Flash a message depending on if the `FLASH_MESSAGES` configuration | |
385 | value is set. | |
386 | ||
387 | :param message: The flash message | |
388 | :param category: The flash message category | |
389 | """ | |
390 | if config_value("FLASH_MESSAGES"): | |
391 | flash(message, category) | |
392 | ||
393 | ||
394 | def get_url(endpoint_or_url, qparams=None): | |
395 | """Returns a URL if a valid endpoint is found. Otherwise, returns the | |
396 | provided value. | |
397 | ||
398 | :param endpoint_or_url: The endpoint name or URL to default to | |
399 | :param qparams: additional query params to add to end of url | |
400 | :return: URL | |
401 | """ | |
402 | try: | |
403 | return transform_url(url_for(endpoint_or_url), qparams) | |
404 | except Exception: | |
405 | # This is an external URL (no endpoint defined in app) | |
406 | # For (mostly) testing - allow changing/adding the url - for example | |
407 | # add a different host:port for cases where the UI is running | |
408 | # separately. | |
409 | if _security.redirect_host: | |
410 | url = transform_url( | |
411 | endpoint_or_url, qparams, netloc=_security.redirect_host | |
412 | ) | |
413 | else: | |
414 | url = transform_url(endpoint_or_url, qparams) | |
415 | ||
416 | return url | |
417 | ||
418 | ||
419 | def slash_url_suffix(url, suffix): | |
420 | """Adds a slash either to the beginning or the end of a suffix | |
421 | (which is to be appended to a URL), depending on whether or not | |
422 | the URL ends with a slash.""" | |
423 | ||
424 | return url.endswith("/") and ("%s/" % suffix) or ("/%s" % suffix) | |
425 | ||
426 | ||
427 | def transform_url(url, qparams=None, **kwargs): | |
428 | """ Modify url | |
429 | ||
430 | :param url: url to transform (can be relative) | |
431 | :param qparams: additional query params to add to end of url | |
432 | :param kwargs: pieces of URL to modify - e.g. netloc=localhost:8000 | |
433 | :return: Modified URL | |
434 | ||
435 | .. versionadded:: 3.2.0 | |
436 | """ | |
437 | if not url: | |
438 | return url | |
439 | link_parse = urlsplit(url) | |
440 | if qparams: | |
441 | current_query = dict(parse_qsl(link_parse.query)) | |
442 | current_query.update(qparams) | |
443 | link_parse = link_parse._replace(query=urlencode(current_query)) | |
444 | return urlunsplit(link_parse._replace(**kwargs)) | |
445 | ||
446 | ||
447 | def get_security_endpoint_name(endpoint): | |
448 | return "%s.%s" % (_security.blueprint_name, endpoint) | |
449 | ||
450 | ||
451 | def url_for_security(endpoint, **values): | |
452 | """Return a URL for the security blueprint | |
453 | ||
454 | :param endpoint: the endpoint of the URL (name of the function) | |
455 | :param values: the variable arguments of the URL rule | |
456 | :param _external: if set to `True`, an absolute URL is generated. Server | |
457 | address can be changed via `SERVER_NAME` configuration variable which | |
458 | defaults to `localhost`. | |
459 | :param _anchor: if provided this is added as anchor to the URL. | |
460 | :param _method: if provided this explicitly specifies an HTTP method. | |
461 | """ | |
462 | endpoint = get_security_endpoint_name(endpoint) | |
463 | return url_for(endpoint, **values) | |
464 | ||
465 | ||
466 | def validate_redirect_url(url): | |
467 | if url is None or url.strip() == "": | |
468 | return False | |
469 | url_next = urlsplit(url) | |
470 | url_base = urlsplit(request.host_url) | |
471 | if (url_next.netloc or url_next.scheme) and url_next.netloc != url_base.netloc: | |
472 | return False | |
473 | return True | |
474 | ||
475 | ||
476 | def get_post_action_redirect(config_key, declared=None): | |
477 | urls = [ | |
478 | get_url(request.args.get("next", None)), | |
479 | get_url(request.form.get("next", None)), | |
480 | find_redirect(config_key), | |
481 | ] | |
482 | if declared: | |
483 | urls.insert(0, declared) | |
484 | for url in urls: | |
485 | if validate_redirect_url(url): | |
486 | return url | |
487 | ||
488 | ||
489 | def get_post_login_redirect(declared=None): | |
490 | return get_post_action_redirect("SECURITY_POST_LOGIN_VIEW", declared) | |
491 | ||
492 | ||
493 | def get_post_register_redirect(declared=None): | |
494 | return get_post_action_redirect("SECURITY_POST_REGISTER_VIEW", declared) | |
495 | ||
496 | ||
497 | def get_post_logout_redirect(declared=None): | |
498 | return get_post_action_redirect("SECURITY_POST_LOGOUT_VIEW", declared) | |
499 | ||
500 | ||
501 | def get_post_verify_redirect(declared=None): | |
502 | return get_post_action_redirect("SECURITY_POST_VERIFY_VIEW", declared) | |
503 | ||
504 | ||
505 | def find_redirect(key): | |
506 | """Returns the URL to redirect to after a user logs in successfully. | |
507 | ||
508 | :param key: The session or application configuration key to search for | |
509 | """ | |
510 | rv = ( | |
511 | get_url(session.pop(key.lower(), None)) | |
512 | or get_url(current_app.config[key.upper()] or None) | |
513 | or "/" | |
514 | ) | |
515 | return rv | |
516 | ||
517 | ||
518 | def propagate_next(url): | |
519 | # return either URL or, if URL already has a ?next=xx, return that. | |
520 | url_next = urlsplit(url) | |
521 | qparams = parse_qs(url_next.query) | |
522 | if "next" in qparams: | |
523 | return qparams["next"][0] | |
524 | return url | |
525 | ||
526 | ||
527 | def get_config(app): | |
528 | """Conveniently get the security configuration for the specified | |
529 | application without the annoying 'SECURITY_' prefix. | |
530 | ||
531 | :param app: The application to inspect | |
532 | """ | |
533 | items = app.config.items() | |
534 | prefix = "SECURITY_" | |
535 | ||
536 | def strip_prefix(tup): | |
537 | return (tup[0].replace("SECURITY_", ""), tup[1]) | |
538 | ||
539 | return dict([strip_prefix(i) for i in items if i[0].startswith(prefix)]) | |
540 | ||
541 | ||
542 | def get_message(key, **kwargs): | |
543 | rv = config_value("MSG_" + key) | |
544 | return localize_callback(rv[0], **kwargs), rv[1] | |
545 | ||
546 | ||
547 | def config_value(key, app=None, default=None): | |
548 | """Get a Flask-Security configuration value. | |
549 | ||
550 | :param key: The configuration key without the prefix `SECURITY_` | |
551 | :param app: An optional specific application to inspect. Defaults to | |
552 | Flask's `current_app` | |
553 | :param default: An optional default value if the value is not set | |
554 | """ | |
555 | app = app or current_app | |
556 | return get_config(app).get(key.upper(), default) | |
557 | ||
558 | ||
559 | def get_max_age(key, app=None): | |
560 | td = get_within_delta(key + "_WITHIN", app) | |
561 | return td.seconds + td.days * 24 * 3600 | |
562 | ||
563 | ||
564 | def get_within_delta(key, app=None): | |
565 | """Get a timedelta object from the application configuration following | |
566 | the internal convention of:: | |
567 | ||
568 | <Amount of Units> <Type of Units> | |
569 | ||
570 | Examples of valid config values:: | |
571 | ||
572 | 5 days | |
573 | 10 minutes | |
574 | ||
575 | :param key: The config value key without the `SECURITY_` prefix | |
576 | :param app: Optional application to inspect. Defaults to Flask's | |
577 | `current_app` | |
578 | """ | |
579 | txt = config_value(key, app=app) | |
580 | values = txt.split() | |
581 | return timedelta(**{values[1]: int(values[0])}) | |
582 | ||
583 | ||
584 | def send_mail(subject, recipient, template, **context): | |
585 | """Send an email via the Flask-Mail extension. | |
586 | ||
587 | :param subject: Email subject | |
588 | :param recipient: Email recipient | |
589 | :param template: The name of the email template | |
590 | :param context: The context to render the template with | |
591 | """ | |
592 | ||
593 | context.setdefault("security", _security) | |
594 | context.update(_security._run_ctx_processor("mail")) | |
595 | ||
596 | sender = _security.email_sender | |
597 | if isinstance(sender, LocalProxy): | |
598 | sender = sender._get_current_object() | |
599 | ||
600 | msg = Message(subject, sender=sender, recipients=[recipient]) | |
601 | ||
602 | ctx = ("security/email", template) | |
603 | if config_value("EMAIL_PLAINTEXT"): | |
604 | msg.body = _security.render_template("%s/%s.txt" % ctx, **context) | |
605 | if config_value("EMAIL_HTML"): | |
606 | msg.html = _security.render_template("%s/%s.html" % ctx, **context) | |
607 | ||
608 | if _security._send_mail_task: | |
609 | _security._send_mail_task(msg) | |
610 | return | |
611 | ||
612 | mail = current_app.extensions.get("mail") | |
613 | mail.send(msg) | |
614 | ||
615 | ||
616 | def get_token_status(token, serializer, max_age=None, return_data=False): | |
617 | """Get the status of a token. | |
618 | ||
619 | :param token: The token to check | |
620 | :param serializer: The name of the seriailzer. Can be one of the | |
621 | following: ``confirm``, ``login``, ``reset`` | |
622 | :param max_age: The name of the max age config option. Can be on of | |
623 | the following: ``CONFIRM_EMAIL``, ``LOGIN``, | |
624 | ``RESET_PASSWORD`` | |
625 | """ | |
626 | serializer = getattr(_security, serializer + "_serializer") | |
627 | max_age = get_max_age(max_age) | |
628 | user, data = None, None | |
629 | expired, invalid = False, False | |
630 | ||
631 | try: | |
632 | data = serializer.loads(token, max_age=max_age) | |
633 | except SignatureExpired: | |
634 | d, data = serializer.loads_unsafe(token) | |
635 | expired = True | |
636 | except (BadSignature, TypeError, ValueError): | |
637 | invalid = True | |
638 | ||
639 | if data: | |
640 | user = _datastore.find_user(id=data[0]) | |
641 | ||
642 | expired = expired and (user is not None) | |
643 | ||
644 | if return_data: | |
645 | return expired, invalid, user, data | |
646 | else: | |
647 | return expired, invalid, user | |
648 | ||
649 | ||
650 | def check_and_get_token_status(token, serializer, within=None): | |
651 | """Get the status of a token and return data. | |
652 | ||
653 | :param token: The token to check | |
654 | :param serializer: The name of the serializer. Can be one of the | |
655 | following: ``confirm``, ``login``, ``reset``, ``us_setup`` | |
656 | :param within: max age - passed as a timedelta | |
657 | ||
658 | :return: a tuple of (expired, invalid, data) | |
659 | ||
660 | .. versionadded:: 3.4.0 | |
661 | """ | |
662 | serializer = getattr(_security, serializer + "_serializer") | |
663 | max_age = within.total_seconds() | |
664 | data = None | |
665 | expired, invalid = False, False | |
666 | ||
667 | try: | |
668 | data = serializer.loads(token, max_age=max_age) | |
669 | except SignatureExpired: | |
670 | d, data = serializer.loads_unsafe(token) | |
671 | expired = True | |
672 | except (BadSignature, TypeError, ValueError): | |
673 | invalid = True | |
674 | ||
675 | return expired, invalid, data | |
676 | ||
677 | ||
678 | def get_identity_attributes(app=None): | |
679 | app = app or current_app | |
680 | attrs = app.config["SECURITY_USER_IDENTITY_ATTRIBUTES"] | |
681 | try: | |
682 | attrs = [f.strip() for f in attrs.split(",")] | |
683 | except AttributeError: | |
684 | pass | |
685 | return attrs | |
686 | ||
687 | ||
688 | def uia_phone_mapper(identity): | |
689 | """ Used to match identity as a phone number. This is a simple proxy | |
690 | to :py:class:`PhoneUtil` | |
691 | ||
692 | See :py:data:`SECURITY_USER_IDENTITY_MAPPINGS`. | |
693 | ||
694 | .. versionadded:: 3.4.0 | |
695 | """ | |
696 | ph = _security._phone_util.get_canonical_form(identity) | |
697 | return ph | |
698 | ||
699 | ||
700 | def uia_email_mapper(identity): | |
701 | """ Used to match identity as an email. | |
702 | ||
703 | See :py:data:`SECURITY_USER_IDENTITY_MAPPINGS`. | |
704 | ||
705 | .. versionadded:: 3.4.0 | |
706 | """ | |
707 | ||
708 | # Fake up enough to invoke the WTforms email validator. | |
709 | class FakeField(object): | |
710 | pass | |
711 | ||
712 | email_validator = validators.Email(message="nothing") | |
713 | field = FakeField() | |
714 | setattr(field, "data", identity) | |
715 | try: | |
716 | email_validator(None, field) | |
717 | except ValidationError: | |
718 | return None | |
719 | return identity | |
720 | ||
721 | ||
722 | def use_double_hash(password_hash=None): | |
723 | """Return a bool indicating whether a password should be hashed twice.""" | |
724 | # Default to plaintext for backward compatibility with | |
725 | # SECURITY_PASSWORD_SINGLE_HASH = False | |
726 | single_hash = config_value("PASSWORD_SINGLE_HASH") or {"plaintext"} | |
727 | ||
728 | if password_hash is None: | |
729 | scheme = _security.password_hash | |
730 | else: | |
731 | scheme = _pwd_context.identify(password_hash) | |
732 | ||
733 | return not (single_hash is True or scheme in single_hash) | |
734 | ||
735 | ||
736 | def csrf_cookie_handler(response): | |
737 | """ Called at end of every request. | |
738 | Uses session to track state (set/clear) | |
739 | ||
740 | Ideally we just need to set this once - however by default | |
741 | Flask-WTF has a time-out on these tokens governed by *WTF_CSRF_TIME_LIMIT*. | |
742 | While we could set that to None - and OWASP implies this is fine - that might | |
743 | not be agreeable to everyone. | |
744 | So as a basic usability hack - we check if it is expired and re-generate so at least | |
745 | the user doesn't have to log out and back in (just refresh). | |
746 | We also support a *CSRF_COOKIE_REFRESH_EACH_REQUEST* analogous to Flask's | |
747 | *SESSION_REFRESH_EACH_REQUEST* | |
748 | ||
749 | It is of course removed on logout/session end. | |
750 | Other info on web suggests replacing on every POST and accepting up to 'age' ago. | |
751 | """ | |
752 | csrf_cookie = config_value("CSRF_COOKIE") | |
753 | if not csrf_cookie or not csrf_cookie["key"]: | |
754 | return response | |
755 | ||
756 | op = session.get("fs_cc", None) | |
757 | if not op: | |
758 | remember_cookie_name = current_app.config.get( | |
759 | "REMEMBER_COOKIE_NAME", REMEMBER_COOKIE_NAME | |
760 | ) | |
761 | has_remember_cookie = ( | |
762 | remember_cookie_name in request.cookies | |
763 | and session.get("remember") != "clear" | |
764 | ) | |
765 | # Set cookie if successfully logged in with flask_login's remember cookie | |
766 | if has_remember_cookie and current_user.is_authenticated: | |
767 | op = "set" | |
768 | else: | |
769 | return response | |
770 | ||
771 | if op == "clear": | |
772 | response.delete_cookie( | |
773 | csrf_cookie["key"], | |
774 | path=csrf_cookie.get("path", "/"), | |
775 | domain=csrf_cookie.get("domain", None), | |
776 | ) | |
777 | session.pop("fs_cc") | |
778 | return response | |
779 | ||
780 | # Send a cookie if any of: | |
781 | # 1) CSRF_COOKIE_REFRESH_EACH_REQUEST is true | |
782 | # 2) fs_cc == "set" - this is on first login | |
783 | # 3) existing cookie has expired | |
784 | send = False | |
785 | if op == "set": | |
786 | send = True | |
787 | session["fs_cc"] = "sent" | |
788 | elif config_value("CSRF_COOKIE_REFRESH_EACH_REQUEST"): | |
789 | send = True | |
790 | elif current_app.config["WTF_CSRF_TIME_LIMIT"]: | |
791 | current_cookie = request.cookies.get(csrf_cookie["key"], None) | |
792 | if current_cookie: | |
793 | # Lets make sure it isn't expired if app doesn't set TIME_LIMIT to None. | |
794 | try: | |
795 | csrf.validate_csrf(current_cookie) | |
796 | except ValidationError: | |
797 | send = True | |
798 | ||
799 | if send: | |
800 | kwargs = {k: v for k, v in csrf_cookie.items()} | |
801 | kwargs.pop("key") | |
802 | kwargs["value"] = csrf.generate_csrf() | |
803 | response.set_cookie(csrf_cookie["key"], **kwargs) | |
804 | return response | |
805 | ||
806 | ||
807 | def base_render_json( | |
808 | form, | |
809 | include_user=True, | |
810 | include_auth_token=False, | |
811 | additional=None, | |
812 | error_status_code=400, | |
813 | ): | |
814 | has_errors = len(form.errors) > 0 | |
815 | ||
816 | user = form.user if hasattr(form, "user") else None | |
817 | if has_errors: | |
818 | code = error_status_code | |
819 | payload = json_error_response(errors=form.errors) | |
820 | else: | |
821 | code = 200 | |
822 | payload = dict() | |
823 | if user: | |
824 | # This allows anonymous GETs via JSON | |
825 | if include_user: | |
826 | payload["user"] = user.get_security_payload() | |
827 | ||
828 | if include_auth_token: | |
829 | # view wants to return auth_token - check behavior config | |
830 | if ( | |
831 | config_value("BACKWARDS_COMPAT_AUTH_TOKEN") | |
832 | or "include_auth_token" in request.args | |
833 | ): | |
834 | token = user.get_auth_token() | |
835 | payload["user"]["authentication_token"] = token | |
836 | ||
837 | # Return csrf_token on each JSON response - just as every form | |
838 | # has it rendered. | |
839 | payload["csrf_token"] = csrf.generate_csrf() | |
840 | if additional: | |
841 | payload.update(additional) | |
842 | ||
843 | return _security._render_json(payload, code, headers=None, user=user) | |
844 | ||
845 | ||
846 | def default_want_json(req): | |
847 | """ Return True if response should be in json | |
848 | N.B. do not call this directly - use security.want_json() | |
849 | ||
850 | :param req: Flask/Werkzeug Request | |
851 | """ | |
852 | if req.is_json: | |
853 | return True | |
854 | # TODO should this handle json sub-types? | |
855 | accept_mimetypes = req.accept_mimetypes | |
856 | if not hasattr(req.accept_mimetypes, "best"): # pragma: no cover | |
857 | # Alright. we dont have the best property, lets add it ourselves. | |
858 | # This is for quart compatibility | |
859 | setattr(accept_mimetypes, "best", best) | |
860 | if accept_mimetypes.best == "application/json": | |
861 | return True | |
862 | return False | |
863 | ||
864 | ||
865 | def json_error_response(errors): | |
866 | """ Helper to create an error response that adheres to the openapi spec. | |
867 | """ | |
868 | # Python 2 and 3 compatibility for checking if something is a string. | |
869 | try: # pragma: no cover | |
870 | basestring | |
871 | string_type_check = (basestring, unicode) | |
872 | except NameError: # pragma: no cover | |
873 | string_type_check = str | |
874 | ||
875 | if isinstance(errors, string_type_check): | |
876 | # When the errors is a string, use the response/error/message format | |
877 | response_json = dict(error=errors) | |
878 | elif isinstance(errors, dict): | |
879 | # When the errors is a dict, use the DefaultJsonErrorResponse | |
880 | # (response/errors/name/messages) format | |
881 | response_json = dict(errors=errors) | |
882 | else: | |
883 | raise TypeError("The errors argument should be either a str or dict.") | |
884 | ||
885 | return response_json | |
886 | ||
887 | ||
888 | class FsJsonEncoder(JSONEncoder): | |
889 | """ Flask-Security JSON encoder. | |
890 | Extends Flask's JSONencoder to handle lazy-text. | |
891 | ||
892 | .. versionadded:: 3.3.0 | |
893 | """ | |
894 | ||
895 | def default(self, obj): | |
896 | if is_lazy_string(obj): | |
897 | return str(obj) | |
898 | else: | |
899 | return JSONEncoder.default(self, obj) | |
900 | ||
901 | ||
902 | @contextmanager | |
903 | def capture_passwordless_login_requests(): | |
904 | login_requests = [] | |
905 | ||
906 | def _on(app, **data): | |
907 | login_requests.append(data) | |
908 | ||
909 | login_instructions_sent.connect(_on) | |
910 | ||
911 | try: | |
912 | yield login_requests | |
913 | finally: | |
914 | login_instructions_sent.disconnect(_on) | |
915 | ||
916 | ||
917 | @contextmanager | |
918 | def capture_registrations(): | |
919 | """Testing utility for capturing registrations. | |
920 | """ | |
921 | registrations = [] | |
922 | ||
923 | def _on(app, **data): | |
924 | registrations.append(data) | |
925 | ||
926 | user_registered.connect(_on) | |
927 | ||
928 | try: | |
929 | yield registrations | |
930 | finally: | |
931 | user_registered.disconnect(_on) | |
932 | ||
933 | ||
934 | @contextmanager | |
935 | def capture_reset_password_requests(reset_password_sent_at=None): | |
936 | """Testing utility for capturing password reset requests. | |
937 | ||
938 | :param reset_password_sent_at: An optional datetime object to set the | |
939 | user's `reset_password_sent_at` to | |
940 | """ | |
941 | reset_requests = [] | |
942 | ||
943 | def _on(app, **data): | |
944 | reset_requests.append(data) | |
945 | ||
946 | reset_password_instructions_sent.connect(_on) | |
947 | ||
948 | try: | |
949 | yield reset_requests | |
950 | finally: | |
951 | reset_password_instructions_sent.disconnect(_on) | |
952 | ||
953 | ||
954 | @contextmanager | |
955 | def capture_flashes(): | |
956 | """Testing utility for capturing flashes.""" | |
957 | flashes = [] | |
958 | ||
959 | def _on(app, **data): | |
960 | flashes.append(data) | |
961 | ||
962 | message_flashed.connect(_on) | |
963 | ||
964 | try: | |
965 | yield flashes | |
966 | finally: | |
967 | message_flashed.disconnect(_on) | |
968 | ||
969 | ||
970 | class SmsSenderBaseClass(object): | |
971 | __metaclass__ = abc.ABCMeta | |
972 | ||
973 | def __init__(self): | |
974 | pass | |
975 | ||
976 | @abc.abstractmethod | |
977 | def send_sms(self, from_number, to_number, msg): # pragma: no cover | |
978 | """ Abstract method for sending sms messages | |
979 | ||
980 | .. versionadded:: 3.2.0 | |
981 | """ | |
982 | return | |
983 | ||
984 | ||
985 | class DummySmsSender(SmsSenderBaseClass): | |
986 | def send_sms(self, from_number, to_number, msg): # pragma: no cover | |
987 | """ Do nothing. """ | |
988 | return | |
989 | ||
990 | ||
991 | class SmsSenderFactory(object): | |
992 | senders = {"Dummy": DummySmsSender} | |
993 | ||
994 | @classmethod | |
995 | def createSender(cls, name, *args, **kwargs): | |
996 | """ Initialize an SMS sender. | |
997 | ||
998 | :param name: Name as registered in SmsSenderFactory:senders (e.g. 'Twilio') | |
999 | ||
1000 | .. versionadded:: 3.2.0 | |
1001 | """ | |
1002 | return cls.senders[name](*args, **kwargs) | |
1003 | ||
1004 | ||
1005 | try: # pragma: no cover | |
1006 | from twilio.rest import Client | |
1007 | ||
1008 | class TwilioSmsSender(SmsSenderBaseClass): | |
1009 | def __init__(self): | |
1010 | self.account_sid = config_value("SMS_SERVICE_CONFIG")["ACCOUNT_SID"] | |
1011 | self.auth_token = config_value("SMS_SERVICE_CONFIG")["AUTH_TOKEN"] | |
1012 | ||
1013 | def send_sms(self, from_number, to_number, msg): | |
1014 | """ Send message via twilio account. """ | |
1015 | client = Client(self.account_sid, self.auth_token) | |
1016 | client.messages.create(to=to_number, from_=from_number, body=msg) | |
1017 | ||
1018 | SmsSenderFactory.senders["Twilio"] = TwilioSmsSender | |
1019 | except Exception: | |
1020 | pass | |
1021 | ||
1022 | ||
1023 | def password_length_validator(password): | |
1024 | """ Test password for length. | |
1025 | ||
1026 | :param password: Plain text password to check | |
1027 | ||
1028 | :return: ``None`` if password conforms to length requirements, | |
1029 | a list of error/suggestions if not. | |
1030 | ||
1031 | .. versionadded:: 3.4.0 | |
1032 | ||
1033 | """ | |
1034 | if len(password) < config_value("PASSWORD_LENGTH_MIN") or len(password) > 128: | |
1035 | return [ | |
1036 | get_message( | |
1037 | "PASSWORD_INVALID_LENGTH", length=config_value("PASSWORD_LENGTH_MIN") | |
1038 | )[0] | |
1039 | ] | |
1040 | return None | |
1041 | ||
1042 | ||
1043 | def password_complexity_validator(password, is_register, **kwargs): | |
1044 | """ Test password for complexity. | |
1045 | ||
1046 | Currently just supports 'zxcvbn'. | |
1047 | ||
1048 | :param password: Plain text password to check | |
1049 | :param is_register: if True then kwargs are arbitrary additional info. (e.g. | |
1050 | info from a registration form). If False, must be a SINGLE key "user" that | |
1051 | corresponds to the current_user. All string values will be extracted and | |
1052 | sent to the complexity checker. | |
1053 | :param kwargs: | |
1054 | ||
1055 | :return: ``None`` if password is complex enough, a list of error/suggestions if not. | |
1056 | Be aware that zxcvbn does not (easily) provide a way to localize messages. | |
1057 | ||
1058 | .. versionadded:: 3.4.0 | |
1059 | """ | |
1060 | ||
1061 | if config_value("PASSWORD_COMPLEXITY_CHECKER") == "zxcvbn": | |
1062 | import zxcvbn | |
1063 | ||
1064 | user_info = [] | |
1065 | if not is_register: | |
1066 | for v in kwargs["user"].__dict__.values(): | |
1067 | if v and isinstance(v, str): | |
1068 | user_info.append(v) | |
1069 | else: | |
1070 | # This is usually all register form values that are in the user_model | |
1071 | if kwargs: | |
1072 | user_info = kwargs.values() | |
1073 | results = zxcvbn.zxcvbn(password, user_inputs=user_info) | |
1074 | if results["score"] > 2: | |
1075 | # Good or Strong | |
1076 | return None | |
1077 | # Should we return suggestions? Default forms don't really know what to do. | |
1078 | if results["feedback"]["warning"]: | |
1079 | # Note that these come from zxcvbn and | |
1080 | # aren't localizable via Flask-Security | |
1081 | return [results["feedback"]["warning"]] | |
1082 | return [get_message("PASSWORD_TOO_SIMPLE")[0]] | |
1083 | else: | |
1084 | return None | |
1085 | ||
1086 | ||
1087 | def password_breached_validator(password): | |
1088 | """ Check if password on breached list. | |
1089 | Does nothing unless :py:data:`SECURITY_PASSWORD_CHECK_BREACHED` is set. | |
1090 | If password is found on the breached list, return an error if the count is | |
1091 | greater than or equal to :py:data:`SECURITY_PASSWORD_BREACHED_COUNT`. | |
1092 | ||
1093 | :param password: Plain text password to check | |
1094 | ||
1095 | :return: ``None`` if password passes breached tests, else a list of error messages. | |
1096 | ||
1097 | .. versionadded:: 3.4.0 | |
1098 | """ | |
1099 | pwn = config_value("PASSWORD_CHECK_BREACHED") | |
1100 | if pwn: | |
1101 | try: | |
1102 | cnt = pwned(password) | |
1103 | if cnt >= config_value("PASSWORD_BREACHED_COUNT"): | |
1104 | return [get_message("PASSWORD_BREACHED")[0]] | |
1105 | except Exception: | |
1106 | if pwn == "strict": | |
1107 | return [get_message("PASSWORD_BREACHED_SITE_ERROR")[0]] | |
1108 | return None | |
1109 | ||
1110 | ||
1111 | def default_password_validator(password, is_register, **kwargs): | |
1112 | """ | |
1113 | Password validation. | |
1114 | Called in app/request context. | |
1115 | ||
1116 | N.B. do not call this directly - use security._password_validator | |
1117 | """ | |
1118 | notok = password_length_validator(password) | |
1119 | if notok: | |
1120 | return notok | |
1121 | ||
1122 | notok = password_breached_validator(password) | |
1123 | if notok: | |
1124 | return notok | |
1125 | ||
1126 | return password_complexity_validator(password, is_register, **kwargs) | |
1127 | ||
1128 | ||
1129 | def pwned(password): | |
1130 | """ | |
1131 | Check password against pwnedpasswords API using k-Anonymity. | |
1132 | https://haveibeenpwned.com/API/v3 | |
1133 | ||
1134 | :return: Count of password in DB (0 means hasn't been compromised) | |
1135 | Can raise HTTPError | |
1136 | ||
1137 | Only implemented for python 3 | |
1138 | ||
1139 | .. versionadded:: 3.4.0 | |
1140 | """ | |
1141 | ||
1142 | def convert_password_tuple(value): | |
1143 | hash_suffix, count = value.split(":") | |
1144 | return hash_suffix, int(count) | |
1145 | ||
1146 | sha1 = hashlib.sha1(password.encode("utf8")).hexdigest() | |
1147 | ||
1148 | if PY3: | |
1149 | import urllib.request | |
1150 | import urllib.error | |
1151 | ||
1152 | req = urllib.request.Request( | |
1153 | url="https://api.pwnedpasswords.com/range/{}".format(sha1[:5].upper()), | |
1154 | headers={"User-Agent": "Flask-Security (Python)"}, | |
1155 | ) | |
1156 | # Might raise HTTPError | |
1157 | with urllib.request.urlopen(req) as f: | |
1158 | response = f.read() | |
1159 | ||
1160 | raw = response.decode("utf-8-sig") | |
1161 | ||
1162 | entries = dict(map(convert_password_tuple, raw.upper().split("\r\n"))) | |
1163 | return entries.get(sha1[5:].upper(), 0) | |
1164 | ||
1165 | raise NotImplementedError() |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | flask_security.views | |
3 | ~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Flask-Security views module | |
6 | ||
7 | :copyright: (c) 2012 by Matt Wright. | |
8 | :copyright: (c) 2019-2020 by J. Christopher Wagner (jwag). | |
9 | :license: MIT, see LICENSE for more details. | |
10 | ||
11 | CSRF is tricky. By default all our forms have CSRF protection built in via | |
12 | Flask-WTF. This is regardless of authentication method or whether the request | |
13 | is Form or JSON based. Form-based 'just works' since when rendering the form | |
14 | (on GET), the CSRF token is automatically populated. | |
15 | We want to handle: | |
16 | - JSON requests where CSRF token is in a header (e.g. X-CSRF-Token) | |
17 | - Option to skip CSRF when using a token to authenticate (rather than session) | |
18 | (CSRF_PROTECT_MECHANISMS) | |
19 | - Option to skip CSRF for 'login'/unauthenticated requests | |
20 | (CSRF_IGNORE_UNAUTH_ENDPOINTS) | |
21 | This is complicated by the fact that the only way to disable form CSRF is to | |
22 | pass in meta={csrf: false} at form instantiation time. | |
23 | ||
24 | Be aware that for CSRF to work, caller MUST pass in session cookie. So | |
25 | for pure API, and no session cookie - there is no way to support CSRF-Login | |
26 | so app must set CSRF_IGNORE_UNAUTH_ENDPOINTS (or use CSRF/session cookie for logging | |
27 | in then once they have a token, no need for cookie). | |
28 | ||
29 | TODO: two-factor routes such as tf_setup need work. They seem to support both | |
30 | authenticated (via session?) as well as unauthenticated access. | |
31 | """ | |
32 | ||
33 | import sys | |
34 | import time | |
35 | ||
36 | from flask import ( | |
37 | Blueprint, | |
38 | abort, | |
39 | after_this_request, | |
40 | current_app, | |
41 | jsonify, | |
42 | request, | |
43 | session, | |
44 | ) | |
45 | from flask_login import current_user | |
46 | from werkzeug.datastructures import MultiDict | |
47 | from werkzeug.local import LocalProxy | |
48 | ||
49 | from .changeable import change_user_password | |
50 | from .confirmable import ( | |
51 | confirm_email_token_status, | |
52 | confirm_user, | |
53 | send_confirmation_instructions, | |
54 | ) | |
55 | from .decorators import anonymous_user_required, auth_required, unauth_csrf | |
56 | from .passwordless import login_token_status, send_login_instructions | |
57 | from .quart_compat import get_quart_status | |
58 | from .unified_signin import ( | |
59 | us_signin, | |
60 | us_signin_send_code, | |
61 | us_qrcode, | |
62 | us_setup, | |
63 | us_setup_validate, | |
64 | us_verify, | |
65 | us_verify_link, | |
66 | us_verify_send_code, | |
67 | ) | |
68 | from .recoverable import ( | |
69 | reset_password_token_status, | |
70 | send_reset_password_instructions, | |
71 | update_password, | |
72 | ) | |
73 | from .registerable import register_user | |
74 | from .twofactor import ( | |
75 | complete_two_factor_process, | |
76 | tf_clean_session, | |
77 | tf_disable, | |
78 | tf_login, | |
79 | ) | |
80 | from .utils import ( | |
81 | base_render_json, | |
82 | config_value, | |
83 | do_flash, | |
84 | get_message, | |
85 | get_post_login_redirect, | |
86 | get_post_logout_redirect, | |
87 | get_post_register_redirect, | |
88 | get_post_verify_redirect, | |
89 | get_url, | |
90 | json_error_response, | |
91 | login_user, | |
92 | logout_user, | |
93 | slash_url_suffix, | |
94 | suppress_form_csrf, | |
95 | url_for_security, | |
96 | ) | |
97 | ||
98 | if get_quart_status(): # pragma: no cover | |
99 | from quart import make_response, redirect | |
100 | else: | |
101 | from flask import make_response, redirect | |
102 | ||
103 | # Convenient references | |
104 | _security = LocalProxy(lambda: current_app.extensions["security"]) | |
105 | _datastore = LocalProxy(lambda: _security.datastore) | |
106 | ||
107 | ||
108 | def default_render_json(payload, code, headers, user): | |
109 | """ Default JSON response handler. | |
110 | """ | |
111 | # Force Content-Type header to json. | |
112 | if headers is None: | |
113 | headers = dict() | |
114 | headers["Content-Type"] = "application/json" | |
115 | payload = dict(meta=dict(code=code), response=payload) | |
116 | return make_response(jsonify(payload), code, headers) | |
117 | ||
118 | ||
119 | PY3 = sys.version_info[0] == 3 | |
120 | if PY3 and get_quart_status(): # pragma: no cover | |
121 | from .async_compat import _commit # noqa: F401 | |
122 | else: | |
123 | ||
124 | def _commit(response=None): | |
125 | _datastore.commit() | |
126 | return response | |
127 | ||
128 | ||
129 | def _ctx(endpoint): | |
130 | return _security._run_ctx_processor(endpoint) | |
131 | ||
132 | ||
133 | @unauth_csrf(fall_through=True) | |
134 | def login(): | |
135 | """View function for login view | |
136 | ||
137 | Allow already authenticated users. For GET this is useful for | |
138 | single-page-applications on refresh - session still active but need to | |
139 | access user info and csrf-token. | |
140 | For POST - redirects to POST_LOGIN_VIEW (forms) or returns 400 (json). | |
141 | """ | |
142 | ||
143 | if current_user.is_authenticated and request.method == "POST": | |
144 | # Just redirect current_user to POST_LOGIN_VIEW (or next). | |
145 | # While its tempting to try to logout the current user and login the | |
146 | # new requested user - that simply doesn't work with CSRF. | |
147 | ||
148 | # While this is close to anonymous_user_required - it differs in that | |
149 | # it uses get_post_login_redirect which correctly handles 'next'. | |
150 | # TODO: consider changing anonymous_user_required to also call | |
151 | # get_post_login_redirect - not sure why it never has? | |
152 | if _security._want_json(request): | |
153 | payload = json_error_response( | |
154 | errors=get_message("ANONYMOUS_USER_REQUIRED")[0] | |
155 | ) | |
156 | return _security._render_json(payload, 400, None, None) | |
157 | else: | |
158 | return redirect(get_post_login_redirect()) | |
159 | ||
160 | form_class = _security.login_form | |
161 | ||
162 | if request.is_json: | |
163 | # Allow GET so we can return csrf_token for pre-login. | |
164 | if request.content_length: | |
165 | form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) | |
166 | else: | |
167 | form = form_class(MultiDict([]), meta=suppress_form_csrf()) | |
168 | else: | |
169 | form = form_class(request.form, meta=suppress_form_csrf()) | |
170 | ||
171 | if form.validate_on_submit(): | |
172 | remember_me = form.remember.data if "remember" in form else None | |
173 | if config_value("TWO_FACTOR") and ( | |
174 | config_value("TWO_FACTOR_REQUIRED") | |
175 | or (form.user.tf_totp_secret and form.user.tf_primary_method) | |
176 | ): | |
177 | return tf_login( | |
178 | form.user, remember=remember_me, primary_authn_via="password" | |
179 | ) | |
180 | ||
181 | login_user(form.user, remember=remember_me, authn_via=["password"]) | |
182 | after_this_request(_commit) | |
183 | ||
184 | if not _security._want_json(request): | |
185 | return redirect(get_post_login_redirect()) | |
186 | ||
187 | if _security._want_json(request): | |
188 | if current_user.is_authenticated: | |
189 | form.user = current_user | |
190 | return base_render_json(form, include_auth_token=True) | |
191 | ||
192 | if current_user.is_authenticated: | |
193 | # Basically a no-op if authenticated - just perform the same | |
194 | # post-login redirect as if user just logged in. | |
195 | return redirect(get_post_login_redirect()) | |
196 | else: | |
197 | return _security.render_template( | |
198 | config_value("LOGIN_USER_TEMPLATE"), login_user_form=form, **_ctx("login") | |
199 | ) | |
200 | ||
201 | ||
202 | @auth_required() | |
203 | def verify(): | |
204 | """View function which handles a authentication verification request. | |
205 | """ | |
206 | form_class = _security.verify_form | |
207 | ||
208 | if request.is_json: | |
209 | form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) | |
210 | else: | |
211 | form = form_class(meta=suppress_form_csrf()) | |
212 | ||
213 | if form.validate_on_submit(): | |
214 | # form may have called verify_and_update_password() | |
215 | after_this_request(_commit) | |
216 | ||
217 | # verified - so set freshness time. | |
218 | session["fs_paa"] = time.time() | |
219 | ||
220 | if _security._want_json(request): | |
221 | return base_render_json(form) | |
222 | do_flash(*get_message("REAUTHENTICATION_SUCCESSFUL")) | |
223 | return redirect(get_post_verify_redirect()) | |
224 | ||
225 | if _security._want_json(request): | |
226 | assert form.user == current_user | |
227 | return base_render_json(form) | |
228 | ||
229 | return _security.render_template( | |
230 | config_value("VERIFY_TEMPLATE"), verify_form=form, **_ctx("verify") | |
231 | ) | |
232 | ||
233 | ||
234 | def logout(): | |
235 | """View function which handles a logout request.""" | |
236 | tf_clean_session() | |
237 | ||
238 | if current_user.is_authenticated: | |
239 | logout_user() | |
240 | ||
241 | # No body is required - so if a POST and json - return OK | |
242 | if request.method == "POST" and _security._want_json(request): | |
243 | return _security._render_json({}, 200, headers=None, user=None) | |
244 | ||
245 | return redirect(get_post_logout_redirect()) | |
246 | ||
247 | ||
248 | @anonymous_user_required | |
249 | def register(): | |
250 | """View function which handles a registration request.""" | |
251 | ||
252 | # For some unknown historic reason - if you don't require confirmation | |
253 | # (via email) then you need to type in your password twice. That might | |
254 | # make sense if you can't reset your password but in modern (2020) UX models | |
255 | # don't ask twice. | |
256 | if _security.confirmable or request.is_json: | |
257 | form_class = _security.confirm_register_form | |
258 | else: | |
259 | form_class = _security.register_form | |
260 | ||
261 | if request.is_json: | |
262 | form_data = MultiDict(request.get_json()) | |
263 | else: | |
264 | form_data = request.form | |
265 | ||
266 | form = form_class(form_data, meta=suppress_form_csrf()) | |
267 | if form.validate_on_submit(): | |
268 | did_login = False | |
269 | user = register_user(form) | |
270 | form.user = user | |
271 | ||
272 | # The 'auto-login' feature probably should be removed - I can't imagine | |
273 | # an application that would want random email accounts. It has been like this | |
274 | # since the beginning. Note that we still enforce 2FA - however for unified | |
275 | # signin - we adhere to historic behavior. | |
276 | if not _security.confirmable or _security.login_without_confirmation: | |
277 | if config_value("TWO_FACTOR") and config_value("TWO_FACTOR_REQUIRED"): | |
278 | return tf_login(user, primary_authn_via="register") | |
279 | after_this_request(_commit) | |
280 | login_user(user, authn_via=["register"]) | |
281 | did_login = True | |
282 | ||
283 | if not _security._want_json(request): | |
284 | return redirect(get_post_register_redirect()) | |
285 | ||
286 | # Only include auth token if in fact user is permitted to login | |
287 | return base_render_json(form, include_auth_token=did_login) | |
288 | if _security._want_json(request): | |
289 | return base_render_json(form) | |
290 | ||
291 | return _security.render_template( | |
292 | config_value("REGISTER_USER_TEMPLATE"), | |
293 | register_user_form=form, | |
294 | **_ctx("register") | |
295 | ) | |
296 | ||
297 | ||
298 | @unauth_csrf(fall_through=True) | |
299 | def send_login(): | |
300 | """View function that sends login instructions for passwordless login""" | |
301 | ||
302 | form_class = _security.passwordless_login_form | |
303 | ||
304 | if request.is_json: | |
305 | form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) | |
306 | else: | |
307 | form = form_class(meta=suppress_form_csrf()) | |
308 | ||
309 | if form.validate_on_submit(): | |
310 | send_login_instructions(form.user) | |
311 | if not _security._want_json(request): | |
312 | do_flash(*get_message("LOGIN_EMAIL_SENT", email=form.user.email)) | |
313 | ||
314 | if _security._want_json(request): | |
315 | return base_render_json(form) | |
316 | ||
317 | return _security.render_template( | |
318 | config_value("SEND_LOGIN_TEMPLATE"), send_login_form=form, **_ctx("send_login") | |
319 | ) | |
320 | ||
321 | ||
322 | @anonymous_user_required | |
323 | def token_login(token): | |
324 | """View function that handles passwordless login via a token | |
325 | Like reset-password and confirm - this is usually a GET via an email | |
326 | so from the request we can't differentiate form-based apps from non. | |
327 | """ | |
328 | ||
329 | expired, invalid, user = login_token_status(token) | |
330 | ||
331 | if not user or invalid: | |
332 | m, c = get_message("INVALID_LOGIN_TOKEN") | |
333 | if _security.redirect_behavior == "spa": | |
334 | return redirect(get_url(_security.login_error_view, qparams={c: m})) | |
335 | do_flash(m, c) | |
336 | return redirect(url_for_security("login")) | |
337 | if expired: | |
338 | send_login_instructions(user) | |
339 | m, c = get_message( | |
340 | "LOGIN_EXPIRED", email=user.email, within=_security.login_within | |
341 | ) | |
342 | if _security.redirect_behavior == "spa": | |
343 | return redirect( | |
344 | get_url( | |
345 | _security.login_error_view, | |
346 | qparams=user.get_redirect_qparams({c: m}), | |
347 | ) | |
348 | ) | |
349 | do_flash(m, c) | |
350 | return redirect(url_for_security("login")) | |
351 | ||
352 | login_user(user, authn_via=["token"]) | |
353 | after_this_request(_commit) | |
354 | if _security.redirect_behavior == "spa": | |
355 | return redirect( | |
356 | get_url(_security.post_login_view, qparams=user.get_redirect_qparams()) | |
357 | ) | |
358 | ||
359 | do_flash(*get_message("PASSWORDLESS_LOGIN_SUCCESSFUL")) | |
360 | ||
361 | return redirect(get_post_login_redirect()) | |
362 | ||
363 | ||
364 | @unauth_csrf(fall_through=True) | |
365 | def send_confirmation(): | |
366 | """View function which sends confirmation instructions.""" | |
367 | ||
368 | form_class = _security.send_confirmation_form | |
369 | ||
370 | if request.is_json: | |
371 | form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) | |
372 | else: | |
373 | form = form_class(meta=suppress_form_csrf()) | |
374 | ||
375 | if form.validate_on_submit(): | |
376 | send_confirmation_instructions(form.user) | |
377 | if not _security._want_json(request): | |
378 | do_flash(*get_message("CONFIRMATION_REQUEST", email=form.user.email)) | |
379 | ||
380 | if _security._want_json(request): | |
381 | return base_render_json(form) | |
382 | ||
383 | return _security.render_template( | |
384 | config_value("SEND_CONFIRMATION_TEMPLATE"), | |
385 | send_confirmation_form=form, | |
386 | **_ctx("send_confirmation") | |
387 | ) | |
388 | ||
389 | ||
390 | def confirm_email(token): | |
391 | """View function which handles a email confirmation request.""" | |
392 | ||
393 | expired, invalid, user = confirm_email_token_status(token) | |
394 | ||
395 | if not user or invalid: | |
396 | m, c = get_message("INVALID_CONFIRMATION_TOKEN") | |
397 | if _security.redirect_behavior == "spa": | |
398 | return redirect(get_url(_security.confirm_error_view, qparams={c: m})) | |
399 | do_flash(m, c) | |
400 | return redirect( | |
401 | get_url(_security.confirm_error_view) | |
402 | or url_for_security("send_confirmation") | |
403 | ) | |
404 | ||
405 | already_confirmed = user.confirmed_at is not None | |
406 | ||
407 | if expired or already_confirmed: | |
408 | if already_confirmed: | |
409 | m, c = get_message("ALREADY_CONFIRMED") | |
410 | else: | |
411 | send_confirmation_instructions(user) | |
412 | m, c = get_message( | |
413 | "CONFIRMATION_EXPIRED", | |
414 | email=user.email, | |
415 | within=_security.confirm_email_within, | |
416 | ) | |
417 | ||
418 | if _security.redirect_behavior == "spa": | |
419 | return redirect( | |
420 | get_url( | |
421 | _security.confirm_error_view, | |
422 | qparams=user.get_redirect_qparams({c: m}), | |
423 | ) | |
424 | ) | |
425 | ||
426 | do_flash(m, c) | |
427 | return redirect( | |
428 | get_url(_security.confirm_error_view) | |
429 | or url_for_security("send_confirmation") | |
430 | ) | |
431 | ||
432 | confirm_user(user) | |
433 | after_this_request(_commit) | |
434 | ||
435 | if user != current_user: | |
436 | logout_user() | |
437 | if config_value("AUTO_LOGIN_AFTER_CONFIRM"): | |
438 | # N.B. this is a (small) security risk if email went to wrong place. | |
439 | # and you have the LOGIN_WITH_CONFIRMATION flag since in that case | |
440 | # you can be logged in and doing stuff - but another person could | |
441 | # get the email. | |
442 | if config_value("TWO_FACTOR") and config_value("TWO_FACTOR_REQUIRED"): | |
443 | return tf_login(user, primary_authn_via="confirm") | |
444 | login_user(user, authn_via=["confirm"]) | |
445 | ||
446 | m, c = get_message("EMAIL_CONFIRMED") | |
447 | if _security.redirect_behavior == "spa": | |
448 | return redirect( | |
449 | get_url( | |
450 | _security.post_confirm_view, qparams=user.get_redirect_qparams({c: m}) | |
451 | ) | |
452 | ) | |
453 | do_flash(m, c) | |
454 | return redirect( | |
455 | get_url(_security.post_confirm_view) | |
456 | or get_url( | |
457 | _security.post_login_view | |
458 | if config_value("AUTO_LOGIN_AFTER_CONFIRM") | |
459 | else _security.login_url | |
460 | ) | |
461 | ) | |
462 | ||
463 | ||
464 | @anonymous_user_required | |
465 | @unauth_csrf(fall_through=True) | |
466 | def forgot_password(): | |
467 | """View function that handles a forgotten password request.""" | |
468 | ||
469 | form_class = _security.forgot_password_form | |
470 | ||
471 | if request.is_json: | |
472 | form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) | |
473 | else: | |
474 | form = form_class(meta=suppress_form_csrf()) | |
475 | ||
476 | if form.validate_on_submit(): | |
477 | send_reset_password_instructions(form.user) | |
478 | if not _security._want_json(request): | |
479 | do_flash(*get_message("PASSWORD_RESET_REQUEST", email=form.user.email)) | |
480 | ||
481 | if _security._want_json(request): | |
482 | return base_render_json(form, include_user=False) | |
483 | ||
484 | return _security.render_template( | |
485 | config_value("FORGOT_PASSWORD_TEMPLATE"), | |
486 | forgot_password_form=form, | |
487 | **_ctx("forgot_password") | |
488 | ) | |
489 | ||
490 | ||
491 | @anonymous_user_required | |
492 | @unauth_csrf(fall_through=True) | |
493 | def reset_password(token): | |
494 | """View function that handles a reset password request. | |
495 | ||
496 | This is usually called via GET as part of an email link and redirects to | |
497 | a reset-password form | |
498 | It is called via POST to actually update the password (and then redirects to | |
499 | a post reset/login view) | |
500 | If in either case the token is either invalid or expired it redirects to | |
501 | the 'forgot-password' form. | |
502 | ||
503 | In the case of non-form based configuration: | |
504 | For GET normal case - redirect to RESET_VIEW?token={token}&email={email} | |
505 | For GET invalid case - redirect to RESET_ERROR_VIEW?error={error}&email={email} | |
506 | For POST normal/successful case - return 200 with new authentication token | |
507 | For POST error case return 400 with form.errors | |
508 | """ | |
509 | ||
510 | expired, invalid, user = reset_password_token_status(token) | |
511 | form_class = _security.reset_password_form | |
512 | if request.is_json: | |
513 | form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) | |
514 | else: | |
515 | form = form_class(meta=suppress_form_csrf()) | |
516 | form.user = user | |
517 | ||
518 | if request.method == "GET": | |
519 | if not user or invalid: | |
520 | m, c = get_message("INVALID_RESET_PASSWORD_TOKEN") | |
521 | if _security.redirect_behavior == "spa": | |
522 | return redirect(get_url(_security.reset_error_view, qparams={c: m})) | |
523 | do_flash(m, c) | |
524 | return redirect(url_for_security("forgot_password")) | |
525 | if expired: | |
526 | send_reset_password_instructions(user) | |
527 | m, c = get_message( | |
528 | "PASSWORD_RESET_EXPIRED", | |
529 | email=user.email, | |
530 | within=_security.reset_password_within, | |
531 | ) | |
532 | if _security.redirect_behavior == "spa": | |
533 | return redirect( | |
534 | get_url( | |
535 | _security.reset_error_view, | |
536 | qparams=user.get_redirect_qparams({c: m}), | |
537 | ) | |
538 | ) | |
539 | do_flash(m, c) | |
540 | return redirect(url_for_security("forgot_password")) | |
541 | ||
542 | # All good - for SPA - redirect to the ``reset_view`` | |
543 | if _security.redirect_behavior == "spa": | |
544 | return redirect( | |
545 | get_url( | |
546 | _security.reset_view, | |
547 | qparams=user.get_redirect_qparams({"token": token}), | |
548 | ) | |
549 | ) | |
550 | # for forms - render the reset password form | |
551 | return _security.render_template( | |
552 | config_value("RESET_PASSWORD_TEMPLATE"), | |
553 | reset_password_form=form, | |
554 | reset_password_token=token, | |
555 | **_ctx("reset_password") | |
556 | ) | |
557 | ||
558 | # This is the POST case. | |
559 | m = None | |
560 | if not user or invalid: | |
561 | invalid = True | |
562 | m, c = get_message("INVALID_RESET_PASSWORD_TOKEN") | |
563 | if not _security._want_json(request): | |
564 | do_flash(m, c) | |
565 | ||
566 | if expired: | |
567 | send_reset_password_instructions(user) | |
568 | m, c = get_message( | |
569 | "PASSWORD_RESET_EXPIRED", | |
570 | email=user.email, | |
571 | within=_security.reset_password_within, | |
572 | ) | |
573 | if not _security._want_json(request): | |
574 | do_flash(m, c) | |
575 | ||
576 | if invalid or expired: | |
577 | if _security._want_json(request): | |
578 | return _security._render_json(json_error_response(m), 400, None, None) | |
579 | else: | |
580 | return redirect(url_for_security("forgot_password")) | |
581 | ||
582 | if form.validate_on_submit(): | |
583 | after_this_request(_commit) | |
584 | update_password(user, form.password.data) | |
585 | if config_value("TWO_FACTOR") and ( | |
586 | config_value("TWO_FACTOR_REQUIRED") | |
587 | or (form.user.tf_totp_secret and form.user.tf_primary_method) | |
588 | ): | |
589 | return tf_login(user, primary_authn_via="reset") | |
590 | login_user(user, authn_via=["reset"]) | |
591 | if _security._want_json(request): | |
592 | login_form = _security.login_form(MultiDict({"email": user.email})) | |
593 | setattr(login_form, "user", user) | |
594 | return base_render_json(login_form, include_auth_token=True) | |
595 | else: | |
596 | do_flash(*get_message("PASSWORD_RESET")) | |
597 | return redirect( | |
598 | get_url(_security.post_reset_view) or get_url(_security.post_login_view) | |
599 | ) | |
600 | ||
601 | # validation failure case - for forms - we try again including the token | |
602 | # for non-forms - we just return errors and assume caller remembers token. | |
603 | if _security._want_json(request): | |
604 | return base_render_json(form) | |
605 | return _security.render_template( | |
606 | config_value("RESET_PASSWORD_TEMPLATE"), | |
607 | reset_password_form=form, | |
608 | reset_password_token=token, | |
609 | **_ctx("reset_password") | |
610 | ) | |
611 | ||
612 | ||
613 | @auth_required("basic", "token", "session") | |
614 | def change_password(): | |
615 | """View function which handles a change password request.""" | |
616 | ||
617 | form_class = _security.change_password_form | |
618 | ||
619 | if request.is_json: | |
620 | form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) | |
621 | else: | |
622 | form = form_class(meta=suppress_form_csrf()) | |
623 | ||
624 | if form.validate_on_submit(): | |
625 | after_this_request(_commit) | |
626 | change_user_password(current_user._get_current_object(), form.new_password.data) | |
627 | if not _security._want_json(request): | |
628 | do_flash(*get_message("PASSWORD_CHANGE")) | |
629 | return redirect( | |
630 | get_url(_security.post_change_view) | |
631 | or get_url(_security.post_login_view) | |
632 | ) | |
633 | ||
634 | if _security._want_json(request): | |
635 | form.user = current_user | |
636 | return base_render_json(form, include_auth_token=True) | |
637 | ||
638 | return _security.render_template( | |
639 | config_value("CHANGE_PASSWORD_TEMPLATE"), | |
640 | change_password_form=form, | |
641 | **_ctx("change_password") | |
642 | ) | |
643 | ||
644 | ||
645 | @unauth_csrf(fall_through=True) | |
646 | def two_factor_setup(): | |
647 | """View function for two-factor setup. | |
648 | ||
649 | This is used both for GET to fetch forms and POST to actually set configuration | |
650 | (and send token). | |
651 | ||
652 | There are 3 cases for setting up: | |
653 | 1) initial login and application requires 2FA | |
654 | 2) changing existing 2FA information | |
655 | 3) user wanting to enable or disable 2FA (assuming application doesn't require it) | |
656 | ||
657 | In order to CHANGE/ENABLE/DISABLE a 2FA information, user must be properly logged in | |
658 | AND must perform a fresh password validation by | |
659 | calling POST /tf-confirm (which sets 'tf_confirmed' in the session). | |
660 | ||
661 | For initial login when 2FA required of course user can't be logged in - in this | |
662 | case we need to have been sent some | |
663 | state via the session as part of login to show a) who and b) that they successfully | |
664 | authenticated. | |
665 | """ | |
666 | form_class = _security.two_factor_setup_form | |
667 | ||
668 | if request.is_json: | |
669 | form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) | |
670 | else: | |
671 | form = form_class(meta=suppress_form_csrf()) | |
672 | ||
673 | if not current_user.is_authenticated: | |
674 | # This is the initial login case | |
675 | # We can also get here from setup if they want to change | |
676 | if not all(k in session for k in ["tf_user_id", "tf_state"]) or session[ | |
677 | "tf_state" | |
678 | ] not in ["setup_from_login", "validating_profile"]: | |
679 | # illegal call on this endpoint | |
680 | tf_clean_session() | |
681 | return _tf_illegal_state(form, _security.login_url) | |
682 | ||
683 | user = _datastore.get_user(session["tf_user_id"]) | |
684 | if not user: | |
685 | tf_clean_session() | |
686 | return _tf_illegal_state(form, _security.login_url) | |
687 | ||
688 | else: | |
689 | # all other cases require user to be logged in and have performed | |
690 | # additional password verification as signified by 'tf_confirmed' | |
691 | # in the session. | |
692 | if "tf_confirmed" not in session: | |
693 | tf_clean_session() | |
694 | return _tf_illegal_state(form, _security.two_factor_confirm_url) | |
695 | user = current_user | |
696 | ||
697 | if form.validate_on_submit(): | |
698 | # Before storing in DB and therefore requiring 2FA we need to | |
699 | # make sure it actually works. | |
700 | # Requiring 2FA is triggered by having BOTH tf_totp_secret and | |
701 | # tf_primary_method in the user record (or having the application | |
702 | # global config TWO_FACTOR_REQUIRED) | |
703 | # Until we correctly validate the 2FA - we don't set primary_method in | |
704 | # user model but use the session to store it. | |
705 | pm = form.setup.data | |
706 | if pm == "disable": | |
707 | tf_disable(user) | |
708 | after_this_request(_commit) | |
709 | do_flash(*get_message("TWO_FACTOR_DISABLED")) | |
710 | if not _security._want_json(request): | |
711 | return redirect(get_url(_security.post_login_view)) | |
712 | else: | |
713 | return base_render_json(form) | |
714 | ||
715 | # Regenerate the TOTP secret on every call of 2FA setup unless it is | |
716 | # within the same session and method (e.g. upon entering the phone number) | |
717 | if pm != session.get("tf_primary_method", None): | |
718 | session["tf_totp_secret"] = _security._totp_factory.generate_totp_secret() | |
719 | ||
720 | session["tf_primary_method"] = pm | |
721 | session["tf_state"] = "validating_profile" | |
722 | new_phone = form.phone.data if len(form.phone.data) > 0 else None | |
723 | if new_phone: | |
724 | user.tf_phone_number = new_phone | |
725 | _datastore.put(user) | |
726 | after_this_request(_commit) | |
727 | ||
728 | # This form is sort of bizarre - for SMS and authenticator | |
729 | # you select, then get more info, and submit again. | |
730 | # For authenticator of course, we don't actually send anything | |
731 | # and for SMS it is the second time around that we get the phone number | |
732 | if pm == "email" or (pm == "sms" and new_phone): | |
733 | msg = user.tf_send_security_token( | |
734 | method=pm, | |
735 | totp_secret=session["tf_totp_secret"], | |
736 | phone_number=user.tf_phone_number, | |
737 | ) | |
738 | if msg: | |
739 | # send code didn't work | |
740 | form.setup.errors = list() | |
741 | form.setup.errors.append(msg) | |
742 | if _security._want_json(request): | |
743 | return base_render_json( | |
744 | form, include_user=False, error_status_code=500 | |
745 | ) | |
746 | code_form = _security.two_factor_verify_code_form() | |
747 | if not _security._want_json(request): | |
748 | return _security.render_template( | |
749 | config_value("TWO_FACTOR_SETUP_TEMPLATE"), | |
750 | two_factor_setup_form=form, | |
751 | two_factor_verify_code_form=code_form, | |
752 | choices=config_value("TWO_FACTOR_ENABLED_METHODS"), | |
753 | chosen_method=pm, | |
754 | **_ctx("tf_setup") | |
755 | ) | |
756 | ||
757 | # We get here on GET and POST with failed validation. | |
758 | # For things like phone number - we've already done one POST | |
759 | # that succeeded and now if failed - so retain the initial info | |
760 | if _security._want_json(request): | |
761 | return base_render_json(form, include_user=False) | |
762 | ||
763 | code_form = _security.two_factor_verify_code_form() | |
764 | choices = config_value("TWO_FACTOR_ENABLED_METHODS") | |
765 | if not config_value("TWO_FACTOR_REQUIRED"): | |
766 | choices.append("disable") | |
767 | ||
768 | return _security.render_template( | |
769 | config_value("TWO_FACTOR_SETUP_TEMPLATE"), | |
770 | two_factor_setup_form=form, | |
771 | two_factor_verify_code_form=code_form, | |
772 | choices=choices, | |
773 | chosen_method=form.setup.data, | |
774 | two_factor_required=config_value("TWO_FACTOR_REQUIRED"), | |
775 | **_ctx("tf_setup") | |
776 | ) | |
777 | ||
778 | ||
779 | @unauth_csrf(fall_through=True) | |
780 | def two_factor_token_validation(): | |
781 | """View function for two-factor token validation | |
782 | ||
783 | Two cases: | |
784 | 1) normal login case - everything setup correctly; normal 2FA validation | |
785 | In this case - user not logged in - | |
786 | but 'tf_state' == 'ready' or 'validating_profile' | |
787 | 2) validating after CHANGE/ENABLE 2FA. In this case user logged in/authenticated | |
788 | they must have 'tf_confirmed' set meaning they re-entered their passwd | |
789 | ||
790 | """ | |
791 | ||
792 | form_class = _security.two_factor_verify_code_form | |
793 | ||
794 | if request.is_json: | |
795 | form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) | |
796 | else: | |
797 | form = form_class(meta=suppress_form_csrf()) | |
798 | ||
799 | changing = current_user.is_authenticated | |
800 | if not changing: | |
801 | # This is the normal login case | |
802 | if ( | |
803 | not all(k in session for k in ["tf_user_id", "tf_state"]) | |
804 | or session["tf_state"] not in ["ready", "validating_profile"] | |
805 | or ( | |
806 | session["tf_state"] == "validating_profile" | |
807 | and "tf_primary_method" not in session | |
808 | ) | |
809 | ): | |
810 | # illegal call on this endpoint | |
811 | tf_clean_session() | |
812 | return _tf_illegal_state(form, _security.login_url) | |
813 | ||
814 | user = _datastore.get_user(session["tf_user_id"]) | |
815 | form.user = user | |
816 | if not user: | |
817 | tf_clean_session() | |
818 | return _tf_illegal_state(form, _security.login_url) | |
819 | ||
820 | if session["tf_state"] == "ready": | |
821 | pm = user.tf_primary_method | |
822 | totp_secret = user.tf_totp_secret | |
823 | else: | |
824 | pm = session["tf_primary_method"] | |
825 | totp_secret = session["tf_totp_secret"] | |
826 | else: | |
827 | if ( | |
828 | not all( | |
829 | k in session for k in ["tf_confirmed", "tf_state", "tf_primary_method"] | |
830 | ) | |
831 | or session["tf_state"] != "validating_profile" | |
832 | ): | |
833 | tf_clean_session() | |
834 | # logout since this seems like attack-ish/logic error | |
835 | logout_user() | |
836 | return _tf_illegal_state(form, _security.login_url) | |
837 | pm = session["tf_primary_method"] | |
838 | totp_secret = session["tf_totp_secret"] | |
839 | form.user = current_user | |
840 | ||
841 | setattr(form, "primary_method", pm) | |
842 | setattr(form, "tf_totp_secret", totp_secret) | |
843 | if form.validate_on_submit(): | |
844 | # Success - log in user and clear all session variables | |
845 | completion_message = complete_two_factor_process( | |
846 | form.user, pm, totp_secret, changing, session.pop("tf_remember_login", None) | |
847 | ) | |
848 | after_this_request(_commit) | |
849 | if not _security._want_json(request): | |
850 | do_flash(*get_message(completion_message)) | |
851 | return redirect(get_post_login_redirect()) | |
852 | ||
853 | # GET or not successful POST | |
854 | if _security._want_json(request): | |
855 | return base_render_json(form) | |
856 | ||
857 | # if we were trying to validate a new method | |
858 | if changing: | |
859 | setup_form = _security.two_factor_setup_form() | |
860 | ||
861 | return _security.render_template( | |
862 | config_value("TWO_FACTOR_SETUP_TEMPLATE"), | |
863 | two_factor_setup_form=setup_form, | |
864 | two_factor_verify_code_form=form, | |
865 | choices=config_value("TWO_FACTOR_ENABLED_METHODS"), | |
866 | **_ctx("tf_setup") | |
867 | ) | |
868 | ||
869 | # if we were trying to validate an existing method | |
870 | else: | |
871 | rescue_form = _security.two_factor_rescue_form() | |
872 | ||
873 | return _security.render_template( | |
874 | config_value("TWO_FACTOR_VERIFY_CODE_TEMPLATE"), | |
875 | two_factor_rescue_form=rescue_form, | |
876 | two_factor_verify_code_form=form, | |
877 | problem=None, | |
878 | **_ctx("tf_token_validation") | |
879 | ) | |
880 | ||
881 | ||
882 | @anonymous_user_required | |
883 | @unauth_csrf(fall_through=True) | |
884 | def two_factor_rescue(): | |
885 | """ Function that handles a situation where user can't | |
886 | enter his two-factor validation code | |
887 | ||
888 | User must have already provided valid username/password. | |
889 | User must have already established 2FA | |
890 | ||
891 | """ | |
892 | ||
893 | form_class = _security.two_factor_rescue_form | |
894 | ||
895 | if request.is_json: | |
896 | form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) | |
897 | else: | |
898 | form = form_class(meta=suppress_form_csrf()) | |
899 | ||
900 | if ( | |
901 | not all(k in session for k in ["tf_user_id", "tf_state"]) | |
902 | or session["tf_state"] != "ready" | |
903 | ): | |
904 | tf_clean_session() | |
905 | return _tf_illegal_state(form, _security.login_url) | |
906 | ||
907 | user = _datastore.get_user(session["tf_user_id"]) | |
908 | form.user = user | |
909 | if not user: | |
910 | tf_clean_session() | |
911 | return _tf_illegal_state(form, _security.login_url) | |
912 | ||
913 | rproblem = "" | |
914 | if form.validate_on_submit(): | |
915 | problem = form.data["help_setup"] | |
916 | rproblem = problem | |
917 | # if the problem is that user can't access his device, w | |
918 | # e send him code through mail | |
919 | if problem == "lost_device": | |
920 | msg = form.user.tf_send_security_token( | |
921 | method="email", | |
922 | totp_secret=form.user.tf_totp_secret, | |
923 | phone_number=form.user.tf_phone_number, | |
924 | ) | |
925 | if msg: | |
926 | rproblem = "" | |
927 | form.help_setup.errors.append(msg) | |
928 | if _security._want_json(request): | |
929 | return base_render_json( | |
930 | form, include_user=False, error_status_code=500 | |
931 | ) | |
932 | # send app provider a mail message regarding trouble | |
933 | elif problem == "no_mail_access": | |
934 | _security._send_mail( | |
935 | config_value("EMAIL_SUBJECT_TWO_FACTOR_RESCUE"), | |
936 | config_value("TWO_FACTOR_RESCUE_MAIL"), | |
937 | "two_factor_rescue", | |
938 | user=form.user, | |
939 | ) | |
940 | else: | |
941 | return "", 404 | |
942 | ||
943 | if _security._want_json(request): | |
944 | return base_render_json(form, include_user=False) | |
945 | ||
946 | code_form = _security.two_factor_verify_code_form() | |
947 | return _security.render_template( | |
948 | config_value("TWO_FACTOR_VERIFY_CODE_TEMPLATE"), | |
949 | two_factor_verify_code_form=code_form, | |
950 | two_factor_rescue_form=form, | |
951 | rescue_mail=config_value("TWO_FACTOR_RESCUE_MAIL"), | |
952 | problem=rproblem, | |
953 | **_ctx("tf_token_validation") | |
954 | ) | |
955 | ||
956 | ||
957 | @auth_required("basic", "session", "token") | |
958 | def two_factor_verify_password(): | |
959 | """View function which handles a password verification request.""" | |
960 | form_class = _security.two_factor_verify_password_form | |
961 | ||
962 | if request.is_json: | |
963 | form = form_class(MultiDict(request.get_json()), meta=suppress_form_csrf()) | |
964 | else: | |
965 | form = form_class(meta=suppress_form_csrf()) | |
966 | ||
967 | if form.validate_on_submit(): | |
968 | # form called verify_and_update_password() | |
969 | after_this_request(_commit) | |
970 | session["tf_confirmed"] = True | |
971 | m, c = get_message("TWO_FACTOR_PASSWORD_CONFIRMATION_DONE") | |
972 | if not _security._want_json(request): | |
973 | do_flash(m, c) | |
974 | return redirect(url_for_security("two_factor_setup")) | |
975 | else: | |
976 | return _security._render_json(json_error_response(m), 400, None, None) | |
977 | ||
978 | if _security._want_json(request): | |
979 | assert form.user == current_user | |
980 | # form.user = current_user | |
981 | return base_render_json(form) | |
982 | ||
983 | return _security.render_template( | |
984 | config_value("TWO_FACTOR_VERIFY_PASSWORD_TEMPLATE"), | |
985 | two_factor_verify_password_form=form, | |
986 | **_ctx("tf_verify_password") | |
987 | ) | |
988 | ||
989 | ||
990 | @unauth_csrf(fall_through=True) | |
991 | def two_factor_qrcode(): | |
992 | if current_user.is_authenticated: | |
993 | user = current_user | |
994 | else: | |
995 | if "tf_user_id" not in session: | |
996 | abort(404) | |
997 | user = _datastore.get_user(session["tf_user_id"]) | |
998 | if not user: | |
999 | # Seems like we should be careful here if user_id is gone. | |
1000 | tf_clean_session() | |
1001 | abort(404) | |
1002 | ||
1003 | if "authenticator" not in config_value("TWO_FACTOR_ENABLED_METHODS"): | |
1004 | return abort(404) | |
1005 | if ( | |
1006 | "tf_primary_method" not in session | |
1007 | or session["tf_primary_method"] != "authenticator" | |
1008 | ): | |
1009 | return abort(404) | |
1010 | ||
1011 | totp = user.tf_totp_secret | |
1012 | if "tf_totp_secret" in session: | |
1013 | totp = session["tf_totp_secret"] | |
1014 | try: | |
1015 | import pyqrcode | |
1016 | ||
1017 | # By convention, the URI should have the username that the user | |
1018 | # logs in with. | |
1019 | username = user.calc_username() | |
1020 | url = pyqrcode.create( | |
1021 | _security._totp_factory.get_totp_uri( | |
1022 | username if username else "Unknown", totp | |
1023 | ) | |
1024 | ) | |
1025 | except ImportError: | |
1026 | # For TWO_FACTOR - this should have been checked at app init. | |
1027 | raise | |
1028 | from io import BytesIO | |
1029 | ||
1030 | stream = BytesIO() | |
1031 | url.svg(stream, scale=3) | |
1032 | return ( | |
1033 | stream.getvalue(), | |
1034 | 200, | |
1035 | { | |
1036 | "Content-Type": "image/svg+xml", | |
1037 | "Cache-Control": "no-cache, no-store, must-revalidate", | |
1038 | "Pragma": "no-cache", | |
1039 | "Expires": "0", | |
1040 | }, | |
1041 | ) | |
1042 | ||
1043 | ||
1044 | def _tf_illegal_state(form, redirect_to): | |
1045 | m, c = get_message("TWO_FACTOR_PERMISSION_DENIED") | |
1046 | if not _security._want_json(request): | |
1047 | do_flash(m, c) | |
1048 | return redirect(get_url(redirect_to)) | |
1049 | else: | |
1050 | return _security._render_json(json_error_response(m), 400, None, None) | |
1051 | ||
1052 | ||
1053 | def create_blueprint(app, state, import_name, json_encoder=None): | |
1054 | """Creates the security extension blueprint""" | |
1055 | ||
1056 | bp = Blueprint( | |
1057 | state.blueprint_name, | |
1058 | import_name, | |
1059 | url_prefix=state.url_prefix, | |
1060 | subdomain=state.subdomain, | |
1061 | template_folder="templates", | |
1062 | ) | |
1063 | if json_encoder: | |
1064 | bp.json_encoder = json_encoder | |
1065 | ||
1066 | if state.logout_methods is not None: | |
1067 | bp.route(state.logout_url, methods=state.logout_methods, endpoint="logout")( | |
1068 | logout | |
1069 | ) | |
1070 | ||
1071 | if state.passwordless: | |
1072 | bp.route(state.login_url, methods=["GET", "POST"], endpoint="login")(send_login) | |
1073 | bp.route( | |
1074 | state.login_url + slash_url_suffix(state.login_url, "<token>"), | |
1075 | endpoint="token_login", | |
1076 | )(token_login) | |
1077 | elif config_value("US_SIGNIN_REPLACES_LOGIN", app=app): | |
1078 | bp.route(state.login_url, methods=["GET", "POST"], endpoint="login")(us_signin) | |
1079 | ||
1080 | else: | |
1081 | bp.route(state.login_url, methods=["GET", "POST"], endpoint="login")(login) | |
1082 | ||
1083 | bp.route(state.verify_url, methods=["GET", "POST"], endpoint="verify")(verify) | |
1084 | ||
1085 | if state.unified_signin: | |
1086 | bp.route(state.us_signin_url, methods=["GET", "POST"], endpoint="us_signin")( | |
1087 | us_signin | |
1088 | ) | |
1089 | bp.route( | |
1090 | state.us_signin_send_code_url, | |
1091 | methods=["GET", "POST"], | |
1092 | endpoint="us_signin_send_code", | |
1093 | )(us_signin_send_code) | |
1094 | bp.route(state.us_setup_url, methods=["GET", "POST"], endpoint="us_setup")( | |
1095 | us_setup | |
1096 | ) | |
1097 | bp.route( | |
1098 | state.us_setup_url + slash_url_suffix(state.us_setup_url, "<token>"), | |
1099 | methods=["GET", "POST"], | |
1100 | endpoint="us_setup_validate", | |
1101 | )(us_setup_validate) | |
1102 | ||
1103 | # Freshness verification | |
1104 | if config_value("FRESHNESS", app=app).total_seconds() >= 0: | |
1105 | bp.route( | |
1106 | state.us_verify_url, methods=["GET", "POST"], endpoint="us_verify" | |
1107 | )(us_verify) | |
1108 | bp.route( | |
1109 | state.us_verify_send_code_url, | |
1110 | methods=["GET", "POST"], | |
1111 | endpoint="us_verify_send_code", | |
1112 | )(us_verify_send_code) | |
1113 | ||
1114 | bp.route(state.us_verify_link_url, methods=["GET"], endpoint="us_verify_link")( | |
1115 | us_verify_link | |
1116 | ) | |
1117 | bp.route( | |
1118 | state.us_qrcode_url + slash_url_suffix(state.us_setup_url, "<token>"), | |
1119 | endpoint="us_qrcode", | |
1120 | )(us_qrcode) | |
1121 | ||
1122 | if state.two_factor: | |
1123 | tf_token_validation = "two_factor_token_validation" | |
1124 | tf_qrcode = "two_factor_qrcode" | |
1125 | bp.route( | |
1126 | state.two_factor_setup_url, | |
1127 | methods=["GET", "POST"], | |
1128 | endpoint="two_factor_setup", | |
1129 | )(two_factor_setup) | |
1130 | bp.route( | |
1131 | state.two_factor_token_validation_url, | |
1132 | methods=["GET", "POST"], | |
1133 | endpoint=tf_token_validation, | |
1134 | )(two_factor_token_validation) | |
1135 | bp.route(state.two_factor_qrcode_url, endpoint=tf_qrcode)(two_factor_qrcode) | |
1136 | bp.route( | |
1137 | state.two_factor_rescue_url, | |
1138 | methods=["GET", "POST"], | |
1139 | endpoint="two_factor_rescue", | |
1140 | )(two_factor_rescue) | |
1141 | bp.route( | |
1142 | state.two_factor_confirm_url, | |
1143 | methods=["GET", "POST"], | |
1144 | endpoint="two_factor_verify_password", | |
1145 | )(two_factor_verify_password) | |
1146 | ||
1147 | if state.registerable: | |
1148 | bp.route(state.register_url, methods=["GET", "POST"], endpoint="register")( | |
1149 | register | |
1150 | ) | |
1151 | ||
1152 | if state.recoverable: | |
1153 | bp.route(state.reset_url, methods=["GET", "POST"], endpoint="forgot_password")( | |
1154 | forgot_password | |
1155 | ) | |
1156 | bp.route( | |
1157 | state.reset_url + slash_url_suffix(state.reset_url, "<token>"), | |
1158 | methods=["GET", "POST"], | |
1159 | endpoint="reset_password", | |
1160 | )(reset_password) | |
1161 | ||
1162 | if state.changeable: | |
1163 | bp.route(state.change_url, methods=["GET", "POST"], endpoint="change_password")( | |
1164 | change_password | |
1165 | ) | |
1166 | ||
1167 | if state.confirmable: | |
1168 | bp.route( | |
1169 | state.confirm_url, methods=["GET", "POST"], endpoint="send_confirmation" | |
1170 | )(send_confirmation) | |
1171 | bp.route( | |
1172 | state.confirm_url + slash_url_suffix(state.confirm_url, "<token>"), | |
1173 | methods=["GET", "POST"], | |
1174 | endpoint="confirm_email", | |
1175 | )(confirm_email) | |
1176 | ||
1177 | return bp |
0 | 0 | #!/usr/bin/make -f |
1 | 1 | |
2 | 2 | #export DH_VERBOSE = 1 |
3 | export PYBUILD_BEFORE_TEST=cp -r debian/python-modules/* $(CURDIR)/.pybuild/cpython3_3.9/build/ | |
4 | export PYBUILD_AFTER_TEST=rm -rf $(CURDIR)/.pybuild/cpython3_3.9/build/flask_security | |
5 | export PYBUILD_BEFORE_TEST=cp -r debian/python-modules/* $(CURDIR)/.pybuild/cpython3_3.9/build/faraday | |
6 | export PYBUILD_AFTER_TEST=rm -rf $(CURDIR)/.pybuild/cpython3_3.9/build/faraday/flask_security | |
3 | 7 | export PYBUILD_TEST_ARGS="-k-TestGetExploits -k-test_update_command -k-test_start_and_kill_faraday_server-k-test_create_agent_without_name_fails -k-test_create_agent_invalid_payload -k-test_vuln_filter_exception -k-test_vuln_restless_sort_by_ -k-test_vuln_web_filter_exception -k-test_filter_by_creator_command_id -k-test_openapi_format" |
4 | 8 | |
5 | 9 | %: |
0 | debian/python-modules/flask_security/translations/de_DE/LC_MESSAGES/flask_security.mo | |
1 | debian/python-modules/flask_security/translations/pt_PT/LC_MESSAGES/flask_security.mo | |
2 | debian/python-modules/flask_security/translations/fr_FR/LC_MESSAGES/flask_security.mo | |
3 | debian/python-modules/flask_security/translations/tr_TR/LC_MESSAGES/flask_security.mo | |
4 | debian/python-modules/flask_security/translations/nl_NL/LC_MESSAGES/flask_security.mo | |
5 | debian/python-modules/flask_security/translations/zh_Hans_CN/LC_MESSAGES/flask_security.mo | |
6 | debian/python-modules/flask_security/translations/pt_BR/LC_MESSAGES/flask_security.mo | |
7 | debian/python-modules/flask_security/translations/ru_RU/LC_MESSAGES/flask_security.mo | |
8 | debian/python-modules/flask_security/translations/da_DK/LC_MESSAGES/flask_security.mo | |
9 | debian/python-modules/flask_security/translations/ca_ES/LC_MESSAGES/flask_security.mo | |
10 | debian/python-modules/flask_security/translations/ja_JP/LC_MESSAGES/flask_security.mo | |
11 | debian/python-modules/flask_security/translations/es_ES/LC_MESSAGES/flask_security.mo |