We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Pending invitation links broken in latest version

<!--

We use GitHub only for bug reports 🐛

Anything else should be posted to https://discuss.redash.io 👫

🚨For support, help & questions use https://discuss.redash.io/c/support
💡For feature requests & ideas use https://discuss.redash.io/c/feature-requests

**Found a security vulnerability?** Please email security@redash.io to report any security vulnerabilities. We will acknowledge receipt of your vulnerability and strive to send you regular updates about our progress. If you're curious about the status of your disclosure please feel free to email us again. If you want to encrypt your disclosure email, you can use this PGP key.

-->

### Issue Summary

It looks like the change in #3261 doesn't handle the case of existing users that haven't accepted their invitations yet, so users trying to sign up using invitations created in a previous version incorrectly get the "This invitation has already been accepted" message. https://github.com/getredash/redash/pull/3229/files#diff-a0c5448134fdb627ae48d25bad76393fR40

I was able to fix this in our org with:

```sql
UPDATE users
SET details = '{"is_invitation_pending": true}'::json
WHERE password_hash IS NULL
```

Maybe this case should be handled in the migration here? https://github.com/getredash/redash/blob/master/migrations/versions/e7f8a917aa8e_add_user_details_json_column.py

### Steps to Reproduce

1. Send invitation from a version prior to #3229 and #3261 
2. Upgrade to current master
3. Try to access the invitation link

Any other info e.g. Why do you consider this to be a bug? What did you expect to happen instead?

### Technical details:

* Redash Version:
* Browser/OS:
* How did you install Redash:


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `redash/handlers/authentication.py`
Content:
```
1 import logging
2 
3 from flask import abort, flash, redirect, render_template, request, url_for
4 
5 from flask_login import current_user, login_required, login_user, logout_user
6 from redash import __version__, limiter, models, settings
7 from redash.authentication import current_org, get_login_url, get_next_path
8 from redash.authentication.account import (BadSignature, SignatureExpired,
9                                            send_password_reset_email,
10                                            send_verify_email,
11                                            validate_token)
12 from redash.handlers import routes
13 from redash.handlers.base import json_response, org_scoped_rule
14 from redash.version_check import get_latest_version
15 from sqlalchemy.orm.exc import NoResultFound
16 
17 logger = logging.getLogger(__name__)
18 
19 
20 def get_google_auth_url(next_path):
21     if settings.MULTI_ORG:
22         google_auth_url = url_for('google_oauth.authorize_org', next=next_path, org_slug=current_org.slug)
23     else:
24         google_auth_url = url_for('google_oauth.authorize', next=next_path)
25     return google_auth_url
26 
27 
28 def render_token_login_page(template, org_slug, token):
29     try:
30         user_id = validate_token(token)
31         org = current_org._get_current_object()
32         user = models.User.get_by_id_and_org(user_id, org)
33     except NoResultFound:
34         logger.exception("Bad user id in token. Token= , User id= %s, Org=%s", user_id, token, org_slug)
35         return render_template("error.html", error_message="Invalid invite link. Please ask for a new one."), 400
36     except (SignatureExpired, BadSignature):
37         logger.exception("Failed to verify invite token: %s, org=%s", token, org_slug)
38         return render_template("error.html",
39                                error_message="Your invite link has expired. Please ask for a new one."), 400
40 
41     if not user.is_invitation_pending:
42         return render_template("error.html",
43                                error_message=("This invitation has already been accepted. "
44                                               "Please try resetting your password instead.")), 400
45 
46     status_code = 200
47     if request.method == 'POST':
48         if 'password' not in request.form:
49             flash('Bad Request')
50             status_code = 400
51         elif not request.form['password']:
52             flash('Cannot use empty password.')
53             status_code = 400
54         elif len(request.form['password']) < 6:
55             flash('Password length is too short (<6).')
56             status_code = 400
57         else:
58             user.is_invitation_pending = False
59             user.hash_password(request.form['password'])
60             models.db.session.add(user)
61             login_user(user)
62             models.db.session.commit()
63             return redirect(url_for('redash.index', org_slug=org_slug))
64 
65     google_auth_url = get_google_auth_url(url_for('redash.index', org_slug=org_slug))
66 
67     return render_template(template,
68                            show_google_openid=settings.GOOGLE_OAUTH_ENABLED,
69                            google_auth_url=google_auth_url,
70                            show_saml_login=current_org.get_setting('auth_saml_enabled'),
71                            show_remote_user_login=settings.REMOTE_USER_LOGIN_ENABLED,
72                            show_ldap_login=settings.LDAP_LOGIN_ENABLED,
73                            org_slug=org_slug,
74                            user=user), status_code
75 
76 
77 @routes.route(org_scoped_rule('/invite/<token>'), methods=['GET', 'POST'])
78 def invite(token, org_slug=None):
79     return render_token_login_page("invite.html", org_slug, token)
80 
81 
82 @routes.route(org_scoped_rule('/reset/<token>'), methods=['GET', 'POST'])
83 def reset(token, org_slug=None):
84     return render_token_login_page("reset.html", org_slug, token)
85 
86 
87 @routes.route(org_scoped_rule('/verify/<token>'), methods=['GET'])
88 def verify(token, org_slug=None):
89     try:
90         user_id = validate_token(token)
91         org = current_org._get_current_object()
92         user = models.User.get_by_id_and_org(user_id, org)
93     except (BadSignature, NoResultFound):
94         logger.exception("Failed to verify email verification token: %s, org=%s", token, org_slug)
95         return render_template("error.html",
96                                error_message="Your verification link is invalid. Please ask for a new one."), 400
97 
98     user.is_email_verified = True
99     models.db.session.add(user)
100     models.db.session.commit()
101 
102     return render_template("verify.html", org_slug=org_slug)
103 
104 
105 @routes.route(org_scoped_rule('/forgot'), methods=['GET', 'POST'])
106 def forgot_password(org_slug=None):
107     if not current_org.get_setting('auth_password_login_enabled'):
108         abort(404)
109 
110     submitted = False
111     if request.method == 'POST' and request.form['email']:
112         submitted = True
113         email = request.form['email']
114         try:
115             org = current_org._get_current_object()
116             user = models.User.get_by_email_and_org(email, org)
117             send_password_reset_email(user)
118         except NoResultFound:
119             logging.error("No user found for forgot password: %s", email)
120 
121     return render_template("forgot.html", submitted=submitted)
122 
123 
124 @routes.route(org_scoped_rule('/verification_email'), methods=['POST'])
125 def verification_email(org_slug=None):
126     if not current_user.is_email_verified:
127         send_verify_email(current_user, current_org)
128 
129     return json_response({
130         "message": "Please check your email inbox in order to verify your email address."
131     })
132 
133 
134 @routes.route(org_scoped_rule('/login'), methods=['GET', 'POST'])
135 @limiter.limit(settings.THROTTLE_LOGIN_PATTERN)
136 def login(org_slug=None):
137     # We intentionally use == as otherwise it won't actually use the proxy. So weird :O
138     # noinspection PyComparisonWithNone
139     if current_org == None and not settings.MULTI_ORG:
140         return redirect('/setup')
141     elif current_org == None:
142         return redirect('/')
143 
144     index_url = url_for('redash.index', org_slug=org_slug)
145     unsafe_next_path = request.args.get('next', index_url)
146     next_path = get_next_path(unsafe_next_path)
147     if current_user.is_authenticated:
148         return redirect(next_path)
149 
150     if request.method == 'POST':
151         try:
152             org = current_org._get_current_object()
153             user = models.User.get_by_email_and_org(request.form['email'], org)
154             if user and not user.is_disabled and user.verify_password(request.form['password']):
155                 remember = ('remember' in request.form)
156                 login_user(user, remember=remember)
157                 return redirect(next_path)
158             else:
159                 flash("Wrong email or password.")
160         except NoResultFound:
161             flash("Wrong email or password.")
162 
163     google_auth_url = get_google_auth_url(next_path)
164 
165     return render_template("login.html",
166                            org_slug=org_slug,
167                            next=next_path,
168                            email=request.form.get('email', ''),
169                            show_google_openid=settings.GOOGLE_OAUTH_ENABLED,
170                            google_auth_url=google_auth_url,
171                            show_password_login=current_org.get_setting('auth_password_login_enabled'),
172                            show_saml_login=current_org.get_setting('auth_saml_enabled'),
173                            show_remote_user_login=settings.REMOTE_USER_LOGIN_ENABLED,
174                            show_ldap_login=settings.LDAP_LOGIN_ENABLED)
175 
176 
177 @routes.route(org_scoped_rule('/logout'))
178 def logout(org_slug=None):
179     logout_user()
180     return redirect(get_login_url(next=None))
181 
182 
183 def base_href():
184     if settings.MULTI_ORG:
185         base_href = url_for('redash.index', _external=True, org_slug=current_org.slug)
186     else:
187         base_href = url_for('redash.index', _external=True)
188 
189     return base_href
190 
191 
192 def date_format_config():
193     date_format = current_org.get_setting('date_format')
194     date_format_list = set(["DD/MM/YY", "MM/DD/YY", "YYYY-MM-DD", settings.DATE_FORMAT])
195     return {
196         'dateFormat': date_format,
197         'dateFormatList': list(date_format_list),
198         'dateTimeFormat': "{0} HH:mm".format(date_format),
199     }
200 
201 
202 def client_config():
203     if not current_user.is_api_user() and current_user.is_authenticated:
204         client_config = {
205             'newVersionAvailable': bool(get_latest_version()),
206             'version': __version__
207         }
208     else:
209         client_config = {}
210 
211     defaults = {
212         'allowScriptsInUserInput': settings.ALLOW_SCRIPTS_IN_USER_INPUT,
213         'showPermissionsControl': current_org.get_setting("feature_show_permissions_control"),
214         'allowCustomJSVisualizations': settings.FEATURE_ALLOW_CUSTOM_JS_VISUALIZATIONS,
215         'autoPublishNamedQueries': settings.FEATURE_AUTO_PUBLISH_NAMED_QUERIES,
216         'mailSettingsMissing': settings.MAIL_DEFAULT_SENDER is None,
217         'dashboardRefreshIntervals': settings.DASHBOARD_REFRESH_INTERVALS,
218         'queryRefreshIntervals': settings.QUERY_REFRESH_INTERVALS,
219         'googleLoginEnabled': settings.GOOGLE_OAUTH_ENABLED,
220         'pageSize': settings.PAGE_SIZE,
221         'pageSizeOptions': settings.PAGE_SIZE_OPTIONS,
222     }
223 
224     client_config.update(defaults)
225     client_config.update({
226         'basePath': base_href()
227     })
228     client_config.update(date_format_config())
229 
230     return client_config
231 
232 
233 @routes.route('/api/config', methods=['GET'])
234 def config(org_slug=None):
235     return json_response({
236         'org_slug': current_org.slug,
237         'client_config': client_config()
238     })
239 
240 
241 @routes.route(org_scoped_rule('/api/session'), methods=['GET'])
242 @login_required
243 def session(org_slug=None):
244     if current_user.is_api_user():
245         user = {
246             'permissions': [],
247             'apiKey': current_user.id
248         }
249     else:
250         user = {
251             'profile_image_url': current_user.profile_image_url,
252             'id': current_user.id,
253             'name': current_user.name,
254             'email': current_user.email,
255             'groups': current_user.group_ids,
256             'permissions': current_user.permissions,
257             'is_email_verified': current_user.is_email_verified
258         }
259 
260     return json_response({
261         'user': user,
262         'org_slug': current_org.slug,
263         'client_config': client_config()
264     })
265
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/redash/handlers/authentication.py b/redash/handlers/authentication.py
--- a/redash/handlers/authentication.py
+++ b/redash/handlers/authentication.py
@@ -38,7 +38,7 @@
         return render_template("error.html",
                                error_message="Your invite link has expired. Please ask for a new one."), 400
 
-    if not user.is_invitation_pending:
+    if user.details.get('is_invitation_pending') is False:
         return render_template("error.html",
                                error_message=("This invitation has already been accepted. "
                                               "Please try resetting your password instead.")), 400

{"golden_diff": "diff --git a/redash/handlers/authentication.py b/redash/handlers/authentication.py\n--- a/redash/handlers/authentication.py\n+++ b/redash/handlers/authentication.py\n@@ -38,7 +38,7 @@\n         return render_template(\"error.html\",\n                                error_message=\"Your invite link has expired. Please ask for a new one.\"), 400\n \n-    if not user.is_invitation_pending:\n+    if user.details.get('is_invitation_pending') is False:\n         return render_template(\"error.html\",\n                                error_message=(\"This invitation has already been accepted. \"\n                                               \"Please try resetting your password instead.\")), 400\n", "issue": "Pending invitation links broken in latest version\n\r\n<!--\r\n\r\nWe use GitHub only for bug reports \ud83d\udc1b\r\n\r\nAnything else should be posted to https://discuss.redash.io \ud83d\udc6b\r\n\r\n\ud83d\udea8For support, help & questions use https://discuss.redash.io/c/support\r\n\ud83d\udca1For feature requests & ideas use https://discuss.redash.io/c/feature-requests\r\n\r\n**Found a security vulnerability?** Please email security@redash.io to report any security vulnerabilities. We will acknowledge receipt of your vulnerability and strive to send you regular updates about our progress. If you're curious about the status of your disclosure please feel free to email us again. If you want to encrypt your disclosure email, you can use this PGP key.\r\n\r\n-->\r\n\r\n### Issue Summary\r\n\r\nIt looks like the change in #3261 doesn't handle the case of existing users that haven't accepted their invitations yet, so users trying to sign up using invitations created in a previous version incorrectly get the \"This invitation has already been accepted\" message. https://github.com/getredash/redash/pull/3229/files#diff-a0c5448134fdb627ae48d25bad76393fR40\r\n\r\nI was able to fix this in our org with:\r\n\r\n```sql\r\nUPDATE users\r\nSET details = '{\"is_invitation_pending\": true}'::json\r\nWHERE password_hash IS NULL\r\n```\r\n\r\nMaybe this case should be handled in the migration here? https://github.com/getredash/redash/blob/master/migrations/versions/e7f8a917aa8e_add_user_details_json_column.py\r\n\r\n### Steps to Reproduce\r\n\r\n1. Send invitation from a version prior to #3229 and #3261 \r\n2. Upgrade to current master\r\n3. Try to access the invitation link\r\n\r\nAny other info e.g. Why do you consider this to be a bug? What did you expect to happen instead?\r\n\r\n### Technical details:\r\n\r\n* Redash Version:\r\n* Browser/OS:\r\n* How did you install Redash:\r\n\n", "before_files": [{"content": "import logging\n\nfrom flask import abort, flash, redirect, render_template, request, url_for\n\nfrom flask_login import current_user, login_required, login_user, logout_user\nfrom redash import __version__, limiter, models, settings\nfrom redash.authentication import current_org, get_login_url, get_next_path\nfrom redash.authentication.account import (BadSignature, SignatureExpired,\n                                           send_password_reset_email,\n                                           send_verify_email,\n                                           validate_token)\nfrom redash.handlers import routes\nfrom redash.handlers.base import json_response, org_scoped_rule\nfrom redash.version_check import get_latest_version\nfrom sqlalchemy.orm.exc import NoResultFound\n\nlogger = logging.getLogger(__name__)\n\n\ndef get_google_auth_url(next_path):\n    if settings.MULTI_ORG:\n        google_auth_url = url_for('google_oauth.authorize_org', next=next_path, org_slug=current_org.slug)\n    else:\n        google_auth_url = url_for('google_oauth.authorize', next=next_path)\n    return google_auth_url\n\n\ndef render_token_login_page(template, org_slug, token):\n    try:\n        user_id = validate_token(token)\n        org = current_org._get_current_object()\n        user = models.User.get_by_id_and_org(user_id, org)\n    except NoResultFound:\n        logger.exception(\"Bad user id in token. Token= , User id= %s, Org=%s\", user_id, token, org_slug)\n        return render_template(\"error.html\", error_message=\"Invalid invite link. Please ask for a new one.\"), 400\n    except (SignatureExpired, BadSignature):\n        logger.exception(\"Failed to verify invite token: %s, org=%s\", token, org_slug)\n        return render_template(\"error.html\",\n                               error_message=\"Your invite link has expired. Please ask for a new one.\"), 400\n\n    if not user.is_invitation_pending:\n        return render_template(\"error.html\",\n                               error_message=(\"This invitation has already been accepted. \"\n                                              \"Please try resetting your password instead.\")), 400\n\n    status_code = 200\n    if request.method == 'POST':\n        if 'password' not in request.form:\n            flash('Bad Request')\n            status_code = 400\n        elif not request.form['password']:\n            flash('Cannot use empty password.')\n            status_code = 400\n        elif len(request.form['password']) < 6:\n            flash('Password length is too short (<6).')\n            status_code = 400\n        else:\n            user.is_invitation_pending = False\n            user.hash_password(request.form['password'])\n            models.db.session.add(user)\n            login_user(user)\n            models.db.session.commit()\n            return redirect(url_for('redash.index', org_slug=org_slug))\n\n    google_auth_url = get_google_auth_url(url_for('redash.index', org_slug=org_slug))\n\n    return render_template(template,\n                           show_google_openid=settings.GOOGLE_OAUTH_ENABLED,\n                           google_auth_url=google_auth_url,\n                           show_saml_login=current_org.get_setting('auth_saml_enabled'),\n                           show_remote_user_login=settings.REMOTE_USER_LOGIN_ENABLED,\n                           show_ldap_login=settings.LDAP_LOGIN_ENABLED,\n                           org_slug=org_slug,\n                           user=user), status_code\n\n\n@routes.route(org_scoped_rule('/invite/<token>'), methods=['GET', 'POST'])\ndef invite(token, org_slug=None):\n    return render_token_login_page(\"invite.html\", org_slug, token)\n\n\n@routes.route(org_scoped_rule('/reset/<token>'), methods=['GET', 'POST'])\ndef reset(token, org_slug=None):\n    return render_token_login_page(\"reset.html\", org_slug, token)\n\n\n@routes.route(org_scoped_rule('/verify/<token>'), methods=['GET'])\ndef verify(token, org_slug=None):\n    try:\n        user_id = validate_token(token)\n        org = current_org._get_current_object()\n        user = models.User.get_by_id_and_org(user_id, org)\n    except (BadSignature, NoResultFound):\n        logger.exception(\"Failed to verify email verification token: %s, org=%s\", token, org_slug)\n        return render_template(\"error.html\",\n                               error_message=\"Your verification link is invalid. Please ask for a new one.\"), 400\n\n    user.is_email_verified = True\n    models.db.session.add(user)\n    models.db.session.commit()\n\n    return render_template(\"verify.html\", org_slug=org_slug)\n\n\n@routes.route(org_scoped_rule('/forgot'), methods=['GET', 'POST'])\ndef forgot_password(org_slug=None):\n    if not current_org.get_setting('auth_password_login_enabled'):\n        abort(404)\n\n    submitted = False\n    if request.method == 'POST' and request.form['email']:\n        submitted = True\n        email = request.form['email']\n        try:\n            org = current_org._get_current_object()\n            user = models.User.get_by_email_and_org(email, org)\n            send_password_reset_email(user)\n        except NoResultFound:\n            logging.error(\"No user found for forgot password: %s\", email)\n\n    return render_template(\"forgot.html\", submitted=submitted)\n\n\n@routes.route(org_scoped_rule('/verification_email'), methods=['POST'])\ndef verification_email(org_slug=None):\n    if not current_user.is_email_verified:\n        send_verify_email(current_user, current_org)\n\n    return json_response({\n        \"message\": \"Please check your email inbox in order to verify your email address.\"\n    })\n\n\n@routes.route(org_scoped_rule('/login'), methods=['GET', 'POST'])\n@limiter.limit(settings.THROTTLE_LOGIN_PATTERN)\ndef login(org_slug=None):\n    # We intentionally use == as otherwise it won't actually use the proxy. So weird :O\n    # noinspection PyComparisonWithNone\n    if current_org == None and not settings.MULTI_ORG:\n        return redirect('/setup')\n    elif current_org == None:\n        return redirect('/')\n\n    index_url = url_for('redash.index', org_slug=org_slug)\n    unsafe_next_path = request.args.get('next', index_url)\n    next_path = get_next_path(unsafe_next_path)\n    if current_user.is_authenticated:\n        return redirect(next_path)\n\n    if request.method == 'POST':\n        try:\n            org = current_org._get_current_object()\n            user = models.User.get_by_email_and_org(request.form['email'], org)\n            if user and not user.is_disabled and user.verify_password(request.form['password']):\n                remember = ('remember' in request.form)\n                login_user(user, remember=remember)\n                return redirect(next_path)\n            else:\n                flash(\"Wrong email or password.\")\n        except NoResultFound:\n            flash(\"Wrong email or password.\")\n\n    google_auth_url = get_google_auth_url(next_path)\n\n    return render_template(\"login.html\",\n                           org_slug=org_slug,\n                           next=next_path,\n                           email=request.form.get('email', ''),\n                           show_google_openid=settings.GOOGLE_OAUTH_ENABLED,\n                           google_auth_url=google_auth_url,\n                           show_password_login=current_org.get_setting('auth_password_login_enabled'),\n                           show_saml_login=current_org.get_setting('auth_saml_enabled'),\n                           show_remote_user_login=settings.REMOTE_USER_LOGIN_ENABLED,\n                           show_ldap_login=settings.LDAP_LOGIN_ENABLED)\n\n\n@routes.route(org_scoped_rule('/logout'))\ndef logout(org_slug=None):\n    logout_user()\n    return redirect(get_login_url(next=None))\n\n\ndef base_href():\n    if settings.MULTI_ORG:\n        base_href = url_for('redash.index', _external=True, org_slug=current_org.slug)\n    else:\n        base_href = url_for('redash.index', _external=True)\n\n    return base_href\n\n\ndef date_format_config():\n    date_format = current_org.get_setting('date_format')\n    date_format_list = set([\"DD/MM/YY\", \"MM/DD/YY\", \"YYYY-MM-DD\", settings.DATE_FORMAT])\n    return {\n        'dateFormat': date_format,\n        'dateFormatList': list(date_format_list),\n        'dateTimeFormat': \"{0} HH:mm\".format(date_format),\n    }\n\n\ndef client_config():\n    if not current_user.is_api_user() and current_user.is_authenticated:\n        client_config = {\n            'newVersionAvailable': bool(get_latest_version()),\n            'version': __version__\n        }\n    else:\n        client_config = {}\n\n    defaults = {\n        'allowScriptsInUserInput': settings.ALLOW_SCRIPTS_IN_USER_INPUT,\n        'showPermissionsControl': current_org.get_setting(\"feature_show_permissions_control\"),\n        'allowCustomJSVisualizations': settings.FEATURE_ALLOW_CUSTOM_JS_VISUALIZATIONS,\n        'autoPublishNamedQueries': settings.FEATURE_AUTO_PUBLISH_NAMED_QUERIES,\n        'mailSettingsMissing': settings.MAIL_DEFAULT_SENDER is None,\n        'dashboardRefreshIntervals': settings.DASHBOARD_REFRESH_INTERVALS,\n        'queryRefreshIntervals': settings.QUERY_REFRESH_INTERVALS,\n        'googleLoginEnabled': settings.GOOGLE_OAUTH_ENABLED,\n        'pageSize': settings.PAGE_SIZE,\n        'pageSizeOptions': settings.PAGE_SIZE_OPTIONS,\n    }\n\n    client_config.update(defaults)\n    client_config.update({\n        'basePath': base_href()\n    })\n    client_config.update(date_format_config())\n\n    return client_config\n\n\n@routes.route('/api/config', methods=['GET'])\ndef config(org_slug=None):\n    return json_response({\n        'org_slug': current_org.slug,\n        'client_config': client_config()\n    })\n\n\n@routes.route(org_scoped_rule('/api/session'), methods=['GET'])\n@login_required\ndef session(org_slug=None):\n    if current_user.is_api_user():\n        user = {\n            'permissions': [],\n            'apiKey': current_user.id\n        }\n    else:\n        user = {\n            'profile_image_url': current_user.profile_image_url,\n            'id': current_user.id,\n            'name': current_user.name,\n            'email': current_user.email,\n            'groups': current_user.group_ids,\n            'permissions': current_user.permissions,\n            'is_email_verified': current_user.is_email_verified\n        }\n\n    return json_response({\n        'user': user,\n        'org_slug': current_org.slug,\n        'client_config': client_config()\n    })\n", "path": "redash/handlers/authentication.py"}], "after_files": [{"content": "import logging\n\nfrom flask import abort, flash, redirect, render_template, request, url_for\n\nfrom flask_login import current_user, login_required, login_user, logout_user\nfrom redash import __version__, limiter, models, settings\nfrom redash.authentication import current_org, get_login_url, get_next_path\nfrom redash.authentication.account import (BadSignature, SignatureExpired,\n                                           send_password_reset_email,\n                                           send_verify_email,\n                                           validate_token)\nfrom redash.handlers import routes\nfrom redash.handlers.base import json_response, org_scoped_rule\nfrom redash.version_check import get_latest_version\nfrom sqlalchemy.orm.exc import NoResultFound\n\nlogger = logging.getLogger(__name__)\n\n\ndef get_google_auth_url(next_path):\n    if settings.MULTI_ORG:\n        google_auth_url = url_for('google_oauth.authorize_org', next=next_path, org_slug=current_org.slug)\n    else:\n        google_auth_url = url_for('google_oauth.authorize', next=next_path)\n    return google_auth_url\n\n\ndef render_token_login_page(template, org_slug, token):\n    try:\n        user_id = validate_token(token)\n        org = current_org._get_current_object()\n        user = models.User.get_by_id_and_org(user_id, org)\n    except NoResultFound:\n        logger.exception(\"Bad user id in token. Token= , User id= %s, Org=%s\", user_id, token, org_slug)\n        return render_template(\"error.html\", error_message=\"Invalid invite link. Please ask for a new one.\"), 400\n    except (SignatureExpired, BadSignature):\n        logger.exception(\"Failed to verify invite token: %s, org=%s\", token, org_slug)\n        return render_template(\"error.html\",\n                               error_message=\"Your invite link has expired. Please ask for a new one.\"), 400\n\n    if user.details.get('is_invitation_pending') is False:\n        return render_template(\"error.html\",\n                               error_message=(\"This invitation has already been accepted. \"\n                                              \"Please try resetting your password instead.\")), 400\n\n    status_code = 200\n    if request.method == 'POST':\n        if 'password' not in request.form:\n            flash('Bad Request')\n            status_code = 400\n        elif not request.form['password']:\n            flash('Cannot use empty password.')\n            status_code = 400\n        elif len(request.form['password']) < 6:\n            flash('Password length is too short (<6).')\n            status_code = 400\n        else:\n            user.is_invitation_pending = False\n            user.hash_password(request.form['password'])\n            models.db.session.add(user)\n            login_user(user)\n            models.db.session.commit()\n            return redirect(url_for('redash.index', org_slug=org_slug))\n\n    google_auth_url = get_google_auth_url(url_for('redash.index', org_slug=org_slug))\n\n    return render_template(template,\n                           show_google_openid=settings.GOOGLE_OAUTH_ENABLED,\n                           google_auth_url=google_auth_url,\n                           show_saml_login=current_org.get_setting('auth_saml_enabled'),\n                           show_remote_user_login=settings.REMOTE_USER_LOGIN_ENABLED,\n                           show_ldap_login=settings.LDAP_LOGIN_ENABLED,\n                           org_slug=org_slug,\n                           user=user), status_code\n\n\n@routes.route(org_scoped_rule('/invite/<token>'), methods=['GET', 'POST'])\ndef invite(token, org_slug=None):\n    return render_token_login_page(\"invite.html\", org_slug, token)\n\n\n@routes.route(org_scoped_rule('/reset/<token>'), methods=['GET', 'POST'])\ndef reset(token, org_slug=None):\n    return render_token_login_page(\"reset.html\", org_slug, token)\n\n\n@routes.route(org_scoped_rule('/verify/<token>'), methods=['GET'])\ndef verify(token, org_slug=None):\n    try:\n        user_id = validate_token(token)\n        org = current_org._get_current_object()\n        user = models.User.get_by_id_and_org(user_id, org)\n    except (BadSignature, NoResultFound):\n        logger.exception(\"Failed to verify email verification token: %s, org=%s\", token, org_slug)\n        return render_template(\"error.html\",\n                               error_message=\"Your verification link is invalid. Please ask for a new one.\"), 400\n\n    user.is_email_verified = True\n    models.db.session.add(user)\n    models.db.session.commit()\n\n    return render_template(\"verify.html\", org_slug=org_slug)\n\n\n@routes.route(org_scoped_rule('/forgot'), methods=['GET', 'POST'])\ndef forgot_password(org_slug=None):\n    if not current_org.get_setting('auth_password_login_enabled'):\n        abort(404)\n\n    submitted = False\n    if request.method == 'POST' and request.form['email']:\n        submitted = True\n        email = request.form['email']\n        try:\n            org = current_org._get_current_object()\n            user = models.User.get_by_email_and_org(email, org)\n            send_password_reset_email(user)\n        except NoResultFound:\n            logging.error(\"No user found for forgot password: %s\", email)\n\n    return render_template(\"forgot.html\", submitted=submitted)\n\n\n@routes.route(org_scoped_rule('/verification_email'), methods=['POST'])\ndef verification_email(org_slug=None):\n    if not current_user.is_email_verified:\n        send_verify_email(current_user, current_org)\n\n    return json_response({\n        \"message\": \"Please check your email inbox in order to verify your email address.\"\n    })\n\n\n@routes.route(org_scoped_rule('/login'), methods=['GET', 'POST'])\n@limiter.limit(settings.THROTTLE_LOGIN_PATTERN)\ndef login(org_slug=None):\n    # We intentionally use == as otherwise it won't actually use the proxy. So weird :O\n    # noinspection PyComparisonWithNone\n    if current_org == None and not settings.MULTI_ORG:\n        return redirect('/setup')\n    elif current_org == None:\n        return redirect('/')\n\n    index_url = url_for('redash.index', org_slug=org_slug)\n    unsafe_next_path = request.args.get('next', index_url)\n    next_path = get_next_path(unsafe_next_path)\n    if current_user.is_authenticated:\n        return redirect(next_path)\n\n    if request.method == 'POST':\n        try:\n            org = current_org._get_current_object()\n            user = models.User.get_by_email_and_org(request.form['email'], org)\n            if user and not user.is_disabled and user.verify_password(request.form['password']):\n                remember = ('remember' in request.form)\n                login_user(user, remember=remember)\n                return redirect(next_path)\n            else:\n                flash(\"Wrong email or password.\")\n        except NoResultFound:\n            flash(\"Wrong email or password.\")\n\n    google_auth_url = get_google_auth_url(next_path)\n\n    return render_template(\"login.html\",\n                           org_slug=org_slug,\n                           next=next_path,\n                           email=request.form.get('email', ''),\n                           show_google_openid=settings.GOOGLE_OAUTH_ENABLED,\n                           google_auth_url=google_auth_url,\n                           show_password_login=current_org.get_setting('auth_password_login_enabled'),\n                           show_saml_login=current_org.get_setting('auth_saml_enabled'),\n                           show_remote_user_login=settings.REMOTE_USER_LOGIN_ENABLED,\n                           show_ldap_login=settings.LDAP_LOGIN_ENABLED)\n\n\n@routes.route(org_scoped_rule('/logout'))\ndef logout(org_slug=None):\n    logout_user()\n    return redirect(get_login_url(next=None))\n\n\ndef base_href():\n    if settings.MULTI_ORG:\n        base_href = url_for('redash.index', _external=True, org_slug=current_org.slug)\n    else:\n        base_href = url_for('redash.index', _external=True)\n\n    return base_href\n\n\ndef date_format_config():\n    date_format = current_org.get_setting('date_format')\n    date_format_list = set([\"DD/MM/YY\", \"MM/DD/YY\", \"YYYY-MM-DD\", settings.DATE_FORMAT])\n    return {\n        'dateFormat': date_format,\n        'dateFormatList': list(date_format_list),\n        'dateTimeFormat': \"{0} HH:mm\".format(date_format),\n    }\n\n\ndef client_config():\n    if not current_user.is_api_user() and current_user.is_authenticated:\n        client_config = {\n            'newVersionAvailable': bool(get_latest_version()),\n            'version': __version__\n        }\n    else:\n        client_config = {}\n\n    defaults = {\n        'allowScriptsInUserInput': settings.ALLOW_SCRIPTS_IN_USER_INPUT,\n        'showPermissionsControl': current_org.get_setting(\"feature_show_permissions_control\"),\n        'allowCustomJSVisualizations': settings.FEATURE_ALLOW_CUSTOM_JS_VISUALIZATIONS,\n        'autoPublishNamedQueries': settings.FEATURE_AUTO_PUBLISH_NAMED_QUERIES,\n        'mailSettingsMissing': settings.MAIL_DEFAULT_SENDER is None,\n        'dashboardRefreshIntervals': settings.DASHBOARD_REFRESH_INTERVALS,\n        'queryRefreshIntervals': settings.QUERY_REFRESH_INTERVALS,\n        'googleLoginEnabled': settings.GOOGLE_OAUTH_ENABLED,\n        'pageSize': settings.PAGE_SIZE,\n        'pageSizeOptions': settings.PAGE_SIZE_OPTIONS,\n    }\n\n    client_config.update(defaults)\n    client_config.update({\n        'basePath': base_href()\n    })\n    client_config.update(date_format_config())\n\n    return client_config\n\n\n@routes.route('/api/config', methods=['GET'])\ndef config(org_slug=None):\n    return json_response({\n        'org_slug': current_org.slug,\n        'client_config': client_config()\n    })\n\n\n@routes.route(org_scoped_rule('/api/session'), methods=['GET'])\n@login_required\ndef session(org_slug=None):\n    if current_user.is_api_user():\n        user = {\n            'permissions': [],\n            'apiKey': current_user.id\n        }\n    else:\n        user = {\n            'profile_image_url': current_user.profile_image_url,\n            'id': current_user.id,\n            'name': current_user.name,\n            'email': current_user.email,\n            'groups': current_user.group_ids,\n            'permissions': current_user.permissions,\n            'is_email_verified': current_user.is_email_verified\n        }\n\n    return json_response({\n        'user': user,\n        'org_slug': current_org.slug,\n        'client_config': client_config()\n    })\n", "path": "redash/handlers/authentication.py"}]}