diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b30e98c9b..72cdbbb6c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -19,9 +19,10 @@ jobs: - name: Install gdal run: | - sudo apt-add-repository ppa:ubuntugis/ubuntugis-unstable sudo apt-get update - sudo apt-get install libgdal-dev osmctools gdal-bin libspatialindex-dev + sudo apt-get install -y osmium-tool + sudo apt-get install python3-dev build-essential cmake libboost-dev \ + libexpat1-dev zlib1g-dev libbz2-dev libgdal-dev gdal-bin libspatialindex-dev osmctools - name: Install Dependencies run: | diff --git a/core/settings/contrib.py b/core/settings/contrib.py index 77466cdc5..1bf789911 100644 --- a/core/settings/contrib.py +++ b/core/settings/contrib.py @@ -45,7 +45,7 @@ SOCIAL_AUTH_OPENSTREETMAP_LOGIN_URL = "/osm/login/" SOCIAL_AUTH_OPENSTREETMAP_OAUTH2_KEY = os.getenv("OSM_API_KEY") SOCIAL_AUTH_OPENSTREETMAP_OAUTH2_SECRET = os.getenv("OSM_API_SECRET") -SOCIAL_AUTH_LOGIN_REDIRECT_URL = "/" +SOCIAL_AUTH_LOGIN_REDIRECT_URL = "/authorized" SOCIAL_AUTH_LOGIN_ERROR_URL = "/osm/error" SOCIAL_AUTH_URL_NAMESPACE = "osm" SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = ["username", "first_name", "email"] diff --git a/requirements.txt b/requirements.txt index 9a7faec3a..fa1fc9344 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ mercantile~=0.10.0 psycopg2 python3-openid==3.2.0 social-auth-app-django==5.4.0 -social-auth-core @ git+https://github.com/kshitijrajsharma/social-core.git ### Upgrade this to include osm oauth2 when released +social-auth-core==4.7.0 pytz pyyaml>=5.3 raven diff --git a/ui/app/actions/meta.js b/ui/app/actions/meta.js index ab5ad30a8..527f40310 100644 --- a/ui/app/actions/meta.js +++ b/ui/app/actions/meta.js @@ -1,5 +1,5 @@ import axios from "axios"; -import { LOGIN_SUCCESS, login as _login, logout } from "redux-implicit-oauth2"; +import { LOGIN_SUCCESS, logout as _logout } from "redux-implicit-oauth2"; import { selectAuthToken } from "../selectors"; import types from "."; @@ -32,7 +32,7 @@ if (window.OAUTH_CLIENT_ID == null) { const oauthConfig = { // url: window.EXPORTS_API_URL + "/o/openstreetmap_oauth2", - url: window.EXPORTS_API_URL + "/o/authorize?approval_prompt=auto", + url: window.EXPORTS_API_URL + "/o/authorize?approval_prompt=auto&response_type=token", client: window.OAUTH_CLIENT_ID, redirect: `${window.location.protocol}//${hostname}/authorized` }; @@ -95,7 +95,13 @@ export const fetchGroups = () => (dispatch, getState) => { ); }; -export const login = () => _login(oauthConfig); +export const login = () => { + const { url, client, redirect } = oauthConfig; + window.location.href = + url + + `&client_id=${client}` + + `&redirect_uri=${encodeURIComponent(redirect)}`; +}; export const loginSuccess = (token, expiresAt) => dispatch => dispatch({ @@ -104,4 +110,8 @@ export const loginSuccess = (token, expiresAt) => dispatch => expiresAt }); -export { logout }; +export const logout = () => dispatch => { + localStorage.removeItem("access_token"); + localStorage.removeItem("expires_at"); + dispatch(_logout()); +}; \ No newline at end of file diff --git a/ui/app/components/Auth.js b/ui/app/components/Auth.js index 5cec16a3e..5fe0c6474 100644 --- a/ui/app/components/Auth.js +++ b/ui/app/components/Auth.js @@ -1,33 +1,63 @@ -import { Component } from "react"; +import React from "react"; import { connect } from "react-redux"; +import { withRouter } from "react-router-dom"; import { fetchPermissions, loginSuccess } from "../actions/meta"; import { selectIsLoggedIn, selectLocationHash } from "../selectors"; -import { history } from "../config/store"; -class Auth extends Component { +class Auth extends React.Component { componentDidMount() { const { - fetchPermissions, hash: { access_token, expires_in }, isLoggedIn, - loginSuccess + loginSuccess, + fetchPermissions, + history, + location } = this.props; + if (access_token) { + const expiresAt = expires_in + ? Date.now() + parseInt(expires_in, 10) * 1000 + : null; + + loginSuccess(access_token, expiresAt); + localStorage.setItem("access_token", access_token); + if (expiresAt) localStorage.setItem("expires_at", expiresAt); + + fetchPermissions(); + + window.location.hash = ""; + history.replace(location.pathname); + return; + } + + + if (!isLoggedIn) { + const storedToken = localStorage.getItem("access_token"); + const storedExpiry = localStorage.getItem("expires_at"); + if (storedToken) { + const storedExpiresAt = storedExpiry + ? parseInt(storedExpiry, 10) + : null; + if (!storedExpiresAt || storedExpiresAt > Date.now()) { + loginSuccess(storedToken, storedExpiresAt); + } else { + // expired + localStorage.removeItem("access_token"); + localStorage.removeItem("expires_at"); + } + } + } if (isLoggedIn) { fetchPermissions(); - } else if (access_token != null) { - loginSuccess(access_token, expires_in); - history.replace("/"); } } - componentWillUpdate(nextProps, nextState) { - const { fetchPermissions, isLoggedIn: wasLoggedIn } = this.props; - const { isLoggedIn } = nextProps; + componentDidUpdate(prevProps) { - if (!wasLoggedIn && isLoggedIn) { - fetchPermissions(); + if (!prevProps.isLoggedIn && this.props.isLoggedIn) { + this.props.fetchPermissions(); } } @@ -41,6 +71,9 @@ const mapStateToProps = state => ({ isLoggedIn: selectIsLoggedIn(state) }); -export default connect(mapStateToProps, { fetchPermissions, loginSuccess })( - Auth -); +export default withRouter( + connect( + mapStateToProps, + { fetchPermissions, loginSuccess } + )(Auth) +); \ No newline at end of file diff --git a/ui/app/components/Authorized.js b/ui/app/components/Authorized.js index d56e2e728..0d17b6f89 100644 --- a/ui/app/components/Authorized.js +++ b/ui/app/components/Authorized.js @@ -1,10 +1,41 @@ import React from "react"; -import { FormattedMessage } from "react-intl"; - -export default () => -
- -
; +import { connect } from "react-redux"; +import { withRouter } from "react-router-dom"; +import { loginSuccess } from "../actions/meta"; + +class Authorized extends React.Component { + componentDidMount() { + // grab the hash fragment (e.g. "#access_token=…&expires_in=…") + const hash = window.location.hash.replace(/^#/, ""); + const params = new URLSearchParams(hash); + const token = params.get("access_token"); + const expiresIn = params.get("expires_in"); + + if (token) { + + const expiresAt = expiresIn + ? Date.now() + parseInt(expiresIn, 10) * 1000 + : null; + + + this.props.loginSuccess(token, expiresAt); + + window.location.hash = ""; + this.props.history.replace("/"); + } else { + // no token then bounce back .. + this.props.history.replace("/"); + } + } + + render() { + return null; + } +} + +export default withRouter( + connect( + null, + { loginSuccess } + )(Authorized) +); \ No newline at end of file diff --git a/ui/app/components/Message.js b/ui/app/components/Message.js index 9fb51f7e9..2c38c6231 100644 --- a/ui/app/components/Message.js +++ b/ui/app/components/Message.js @@ -21,13 +21,13 @@ class Message extends Component { return null; } return ( -
-

We have recently upgraded from OAuth 1.0 to 2.0. Please Logout and Login again before use!

- -
- ); +
+

If you are experiencing issues logging in after our fixes, please logout and login again to resolve the issue.

+ +
+ ); } } diff --git a/ui/views.py b/ui/views.py index 2cfd4b98e..5e2f56d41 100644 --- a/ui/views.py +++ b/ui/views.py @@ -23,8 +23,7 @@ def authorized(request): # be logged into the site (and it will be confusing if they are, since # "logging out" of the UI just drops the auth token) auth_logout(request) - return render(request, "ui/authorized.html") - + return v3(request) def login(request): if not request.user.is_authenticated: