Skip to content

Commit

Permalink
Potential fixes for cloud related URL and port issues (#1478)
Browse files Browse the repository at this point in the history
  • Loading branch information
cskaandorp committed Jul 18, 2023
1 parent 31e0c74 commit ced52ed
Show file tree
Hide file tree
Showing 16 changed files with 123 additions and 102 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ Thumbs.db #thumbnail cache on Windows
logs
**/*.backup.*
**/*.back.*
.env.*

node_modules
bower_components
Expand Down
30 changes: 30 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,21 @@ Open the web browser at `localhost:3000`
[1]: https://www.npmjs.com/get-npm
[2]: https://reactjs.org/

### Front end development and connection/CORS issues

In development, when working on the front end, the front- and backend are strictly separated. It is assumed the Flask backend runs on port 5000 and the React front end on port 3000. Deviating from these ports will lead to connection or CORS (Cross-Origin Resource Sharing) issues.

As for CORS issues: it is necessary to precisely define the "allowed origins" in the backend. These origins must reflect the URL(s) used by the front end to call the backend. If correctly configured, they are added to the headers of the backend response, so they can be verified by your browser. If the list with origin-URLs doesn't provide a URL that corresponds with the URL used in the original request of the front end, your request is going to fail. __Setting the allowed origins can be done in the [config file](#full-configuration)__.

You can solve connection/CORS issues by doing the following:
1. Start the backend and verify what port number it's running on (read the first lines of the output once you've started the backend in the terminal).
2. Make sure the front end knows where it can find the backend. React reads a configuration `.env` file in the `/asreview/webapp` folder which tells it to use `http://localhost:5000/`. Override this config file by either adding a local version (e.g. `/asreview/webapp/.env.local`) in which you put the correct backend URL (do not forget the `REACT_APP_API_URL` variable, see the `.env` file) or change the URL in the `.env` file itself.
3. If you are running the front end separate from the backend you need to adjust the CORS's 'allowed origins' parameter in the backend to avoid problems. You can do this by setting the front end URL(s) in the [optional parameters of the config file](#optional-config-parameters) under the "ALLOWED_ORIGINS" key.

Be precise when it comes to URLs/port numbers! In the context of CORS `localhost` is different from `127.0.0.1`, although they are normally referring to the same host.

❗Mac users beware: depending on your version of macOS you may experience troubles with `localhost:5000`. Port 5000 may be in use by "Airplay Receiver" which may (!) cause nondeterministic behavior. If you experience similar issues [switch to a different port](#optional-config-parameters).

#### Formatting and linting

Please make use of Prettier (https://prettier.io/docs/en/install.html) to
Expand Down Expand Up @@ -185,6 +200,19 @@ A number of the keys in the JSON file are standard Flask parameters. The keys th
* EMAIL_CONFIG: configuration of the SMTP email server that is used for email verification. It also allows users to retrieve a new password after forgetting it. Don't forget to enter the reply address (REPLY_ADDRESS) of your system emails. Omit this parameter if system emails for verification and password retrieval are unwanted.
* OAUTH: an authenticated ASReview application may integrate with the OAuth functionality of Github, Orcid and Google. Provide the necessary OAuth login credentails (for [Github](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app), [Orcid](https://info.orcid.org/documentation/api-tutorials/api-tutorial-get-and-authenticated-orcid-id/) en [Google](https://support.google.com/cloud/answer/6158849?hl=en)). Please note that the AUTHORIZATION_URL and TOKEN_URL of the Orcid entry are sandbox-urls, and thus not to be used in production. Omit this parameter if OAuth is unwanted.

#### Optional config parameters

There are three optional parameters available that control what address the ASReview server listens to, and avoid CORS issues:

```json
{
"HOST": "0.0.0.0",
"PORT": 5001,
"ALLOWED_ORIGINS": ["http://localhost:3001"],
}
```
The HOST and PORT determine what address the ASReview server listens to. If this deviates from `localhost` and port 5000, and you run the front end separately, make sure the [front end can find the backend](#front-end-development-and-connectioncors-issues). The ALLOWED_ORIGINS key must be set if you run the front end separately. Put in a list all URLs that your front end uses. This can be more than one URL. Failing to do so will certainly lead to CORS issues.

### Converting an unauthenticated application into an authenticated one

Start the application with authentication enabled for the first time. This ensures the creation of the necessary database. To avoid unwanted user input, shutdown the application.
Expand Down Expand Up @@ -319,3 +347,5 @@ docker build -t asreview/asreview:1.0 .
docker push ghcr.io/asreview/asreview
docker push ghcr.io/asreview/asreview:1.0
```

If you are creating a Docker container that runs the app with a [config file](#full-configuration) do __not forget__ to override the IP-address of the Flask backend. Set the HOST variable to "0.0.0.0" since the default "localhost" can't be reached from outside the container.
13 changes: 12 additions & 1 deletion asreview/_deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import argparse
import functools
import logging
import warnings


Expand All @@ -31,5 +32,15 @@ def wrapper(*args, **kwargs):

class DeprecateAction(argparse.Action):
def __call__(self, parser, namespace, values, option_string=None):
warnings.warn(f"Argument {self.option_strings} is deprecated and is ignored.")
logging.warning(f"Argument {self.option_strings} is deprecated and is ignored.")
delattr(namespace, self.dest)


def mark_deprecated_help_strings(parser, prefix="DEPRECATED"):
for action in parser._actions:
if isinstance(action, DeprecateAction):
h = action.help
if h is None:
action.help = prefix
else:
action.help = prefix + ": " + h
1 change: 1 addition & 0 deletions asreview/webapp/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
REACT_APP_API_URL=http://localhost:5000/
41 changes: 12 additions & 29 deletions asreview/webapp/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
from flask import jsonify
from flask import render_template_string
from flask import request
from flask_cors import CORS
from flask_login import current_user
from flask_login import login_user
from flask_login import logout_user
Expand All @@ -37,44 +36,26 @@
from asreview.webapp.authentication.models import User
from asreview.webapp.authentication.oauth_handler import OAuthHandler

# TODO: I need a folder to stash templates for emails,
# is this the way we should do it? I don't see the point
# of making the end-user decide the exact location.
bp = Blueprint("auth", __name__, url_prefix="/auth")

# NOTE: not too sure about this, what if we are dealing with a
# domain name
ROOT_URL = "http://127.0.0.1:3000"

CORS(
bp,
resources={
r"*": {
"origins": [
ROOT_URL,
"http://localhost:3000"
]
}
},
supports_credentials=True
)


def perform_login_user(user):
"""Helper function to login a user"""
return login_user(user, remember=True, duration=dt.timedelta(days=31))


# TODO: not sure if this file is the right place for this function
def send_forgot_password_email(user, cur_app):
def send_forgot_password_email(user, request, cur_app):
# do not send email in test environment
if not cur_app.testing:
# get necessary information out of user object
name = user.name or "ASReview user"
# email config
config = cur_app.config.get("EMAIL_CONFIG")
# redirect url
url = f"{ROOT_URL}reset_password?user_id={user.id}&token={user.token}"
# get url of front-end
root_url = request.headers.get("Origin")
# create url that will be used in the email
url = f"{root_url}/reset_password?user_id={user.id}&token={user.token}"
# create a mailer
mailer = Mail(cur_app)
# open templates as string and render
Expand All @@ -95,15 +76,17 @@ def send_forgot_password_email(user, cur_app):


# TODO: not sure if this file is the right place for this function
def send_confirm_account_email(user, cur_app):
def send_confirm_account_email(user, request, cur_app):
# do not send email in test environment
if not cur_app.testing:
# get necessary information out of user object
name = user.name or "ASReview user"
# email config
config = cur_app.config.get("EMAIL_CONFIG")
# redirect url
url = f"{ROOT_URL}confirm_account?user_id={user.id}&token={user.token}"
# get url of front-end
root_url = request.headers.get("Origin")
# create url that will be used in the email
url = f"{root_url}/confirm_account?user_id={user.id}&token={user.token}"
# create a mailer
mailer = Mail(cur_app)
# open templates as string and render
Expand Down Expand Up @@ -227,7 +210,7 @@ def signup():
# if applicable
if email_verification:
# send email
send_confirm_account_email(user, current_app)
send_confirm_account_email(user, request, current_app)
# result
result = (
201,
Expand Down Expand Up @@ -340,7 +323,7 @@ def forgot_password():
# store data
DB.session.commit()
# send email
send_forgot_password_email(user, current_app)
send_forgot_password_email(user, request, current_app)
# result
result = (200, f"An email has been sent to {email_address}")

Expand Down
13 changes: 0 additions & 13 deletions asreview/webapp/api/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
from flask import jsonify
from flask import request
from flask import send_file
from flask_cors import CORS
from flask_login import current_user
from sqlalchemy import and_
from werkzeug.exceptions import InternalServerError
Expand Down Expand Up @@ -82,18 +81,6 @@
from asreview.webapp.io import read_data

bp = Blueprint("api", __name__, url_prefix="/api")
CORS(
bp,
resources={
r"*": {
"origins": [
"http://127.0.0.1:3000",
"http://localhost:3000"
]
}
},
supports_credentials=True
)


# error handlers
Expand Down
13 changes: 0 additions & 13 deletions asreview/webapp/api/team.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from flask import Blueprint
from flask import jsonify
from flask_cors import CORS
from flask_login import current_user
from sqlalchemy import and_
from sqlalchemy.exc import SQLAlchemyError
Expand All @@ -12,18 +11,6 @@
from asreview.webapp.authentication.models import User

bp = Blueprint("team", __name__, url_prefix="/api")
CORS(
bp,
resources={
r"*": {
"origins": [
"http://127.0.0.1:3000",
"http://localhost:3000"
]
}
},
supports_credentials=True
)

REQUESTER_FRAUD = {"message": "Request can not made by current user."}

Expand Down
1 change: 0 additions & 1 deletion asreview/webapp/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Smart software for systematic reviews." />
<link rel="apple-touch-icon" href="logo192.png" />
<!-- <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> -->
<title>ASReview LAB - A tool for AI-assisted systematic reviews</title>
</head>
Expand Down
10 changes: 0 additions & 10 deletions asreview/webapp/public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,6 @@
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
Expand Down
7 changes: 7 additions & 0 deletions asreview/webapp/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ import {
} from "./hooks/SettingsHooks";
import { useToggle } from "./hooks/useToggle";

// Ensure that on localhost we use 'localhost' instead of '127.0.0.1'
const currentDomain = window.location.href;
if (currentDomain.includes("127.0.0.1")) {
let newDomain = currentDomain.replace("127.0.0.1", "localhost")
window.location.replace(newDomain);
}

const queryClient = new QueryClient();

const App = (props) => {
Expand Down
4 changes: 2 additions & 2 deletions asreview/webapp/src/Components/SignInForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const SignInForm = (props) => {
};

const handleEnterKey = (e) => {
if (e.key === 'Enter') {
if (e.keyCode === 13) {
handleSubmit(e);
}
};
Expand All @@ -112,7 +112,7 @@ const SignInForm = (props) => {
label="Password"
value={password}
onChange={handlePasswordChange}
onKeyPress={handleEnterKey}
onKeyDown={handleEnterKey}
variant="outlined"
fullWidth
type={returnType()}
Expand Down
7 changes: 7 additions & 0 deletions asreview/webapp/src/Components/SignUpForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ const SignUpForm = (props) => {
navigate("/signin");
};

const handleEnterKey = (e) => {
if (e.keyCode === 13) {
handleSubmit(e);
}
};

return (
<Root>
<Fade in>
Expand Down Expand Up @@ -197,6 +203,7 @@ const SignUpForm = (props) => {
size="small"
fullWidth
type={returnType()}
onKeyDown={handleEnterKey}
value={formik.values.confirmPassword}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
Expand Down
17 changes: 7 additions & 10 deletions asreview/webapp/src/globals.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
// When you're running the development server, the javascript code is always
// pointing to localhost:5000. In all other configurations, the api url point to
// the host domain.

import { useTheme } from "@mui/material/styles";
import { setProject } from "./redux/actions";

import ASReviewLAB_black from "./images/asreview_sub_logo_lab_black_transparent.svg";
import ASReviewLAB_white from "./images/asreview_sub_logo_lab_white_transparent.svg";

export const base_url =
(window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1") &&
window.location.port === "3000"
? "http://localhost:5000/"
: "/";
// URL of backend is configured in an .env file. By default it's
// the same as the front-end URL.
let b_url = "/";
if ((process.env.NODE_ENV !== 'production') && Boolean(process.env.REACT_APP_API_URL)) {
b_url = process.env.REACT_APP_API_URL;
}
export const base_url = b_url;
export const api_url = base_url + "api/";
export const auth_url = base_url + "auth/";
export const collab_url = base_url + "team/";
Expand Down

0 comments on commit ced52ed

Please sign in to comment.