LastFM middleware; Window.onmessage needs origin to prevent clashing between auth popups

This commit is contained in:
James Barnsley
2017-11-03 09:06:41 +13:00
parent 35a61686b3
commit a102c31ff6
10 changed files with 186 additions and 58 deletions

View File

@ -1,5 +1,4 @@
<?php
/**
* Spotify Authentication proxy
*
@ -31,36 +30,41 @@ if (isset($_GET['code'])){
// go get our credentials
$response = getToken($_GET['code']);
$responseArray = json_decode( $response, true );
$response = json_decode( $response, true );
// add our code to the array of credentials, etc
$responseArray['authorization_code'] = $_GET['code'];
$response = json_encode($responseArray);
$response['authorization_code'] = $_GET['code'];
$response['origin'] = "auth_spotify";
// make sure we have a successful response
if( !isset($responseArray['access_token']) ){
if( !isset($response['access_token']) ){
echo 'Error!';
die();
}
// Pass our error back to the popup opener
// Pass our error back to the popup opener
?>
<script type="text/javascript">
window.opener.postMessage( '<?php echo $response ?>', "*");
window.opener.postMessage( '<?php echo json_encode($response) ?>', "*");
window.close();
</script>
<?php
// authorization error
} else if (isset($_GET['error'])){
$response = array(
"error" => $_GET["error"],
"origin" => "auth_spotify"
);
// Pass our error back to the popup opener
?>
<script type="text/javascript">
window.opener.postMessage("{\"error\": \"<?php echo $_GET['error'] ?>\"}", "*");
window.close();
</script>
<?php
// Pass our error back to the popup opener
?>
<script type="text/javascript">
window.opener.postMessage('<?php echo json_encode($response) ?>', "*");
window.close();
</script>
<?php
// refresh existing token
} else if (isset($_GET['action']) && $_GET['action'] == 'refresh' && $_GET['refresh_token']){
@ -171,4 +175,5 @@ function refreshToken($refresh_token){
curl_close ($ch);
return $response;
}
}

9
src/js/bootstrap.js vendored
View File

@ -14,6 +14,7 @@ import coreMiddleware from './services/core/middleware'
import uiMiddleware from './services/ui/middleware'
import pusherMiddleware from './services/pusher/middleware'
import mopidyMiddleware from './services/mopidy/middleware'
import lastfmMiddleware from './services/lastfm/middleware'
import spotifyMiddleware from './services/spotify/middleware'
import localstorageMiddleware from './services/localstorage/middleware'
@ -108,12 +109,18 @@ if (localStorage.getItem('spotify')){
initialState.spotify = Object.assign(initialState.spotify, storedSpotify );
}
// if we've got a stored version of lastfm state, load and merge
if (localStorage.getItem('lastfm')){
var storedLastfm = JSON.parse(localStorage.getItem('lastfm') );
initialState.lastfm = Object.assign(initialState.lastfm, storedLastfm );
}
console.log('Bootstrapping', initialState)
let store = createStore(
reducers,
initialState,
applyMiddleware(thunk, localstorageMiddleware, coreMiddleware, uiMiddleware, mopidyMiddleware, pusherMiddleware, spotifyMiddleware )
applyMiddleware(thunk, localstorageMiddleware, coreMiddleware, uiMiddleware, mopidyMiddleware, pusherMiddleware, spotifyMiddleware, lastfmMiddleware )
);
export default store;

View File

@ -21,20 +21,20 @@ class LastfmAuthenticationFrame extends React.Component{
}
componentDidMount(){
let self = this;
// Listen for incoming messages from the authorization iframe
// This is triggered when the popup posts a message, which is then passed to
// the iframe, and then passed on to the parent frame (our application)
// Listen for incoming messages from the authorization popup
window.addEventListener('message', function(event){
self.handleMessage(event)
var data = JSON.parse(event.data);
// Only digest messages relevant to us
if (data.origin == 'auth_lastfm'){
self.handleMessage(event, data);
}
}, false);
}
handleMessage(event){
var data = JSON.parse(event.data)
handleMessage(event, data){
// Only allow incoming data from our authorized authenticator proxy
var authorization_domain = this.props.authorization_url.substring(0,this.props.authorization_url.indexOf('/',8))
@ -44,13 +44,13 @@ class LastfmAuthenticationFrame extends React.Component{
}
// Bounced with an error
if (typeof(data.error) !== 'undefined'){
if (data.error !== undefined){
this.props.uiActions.createNotification(data.error,'bad')
// No errors? We're in!
} else {
this.props.lastfmActions.authorizationGranted(data)
//this.props.lastfmActions.getMe()
this.props.lastfmActions.getMe()
}
// Turn off our authorizing switch
@ -64,7 +64,8 @@ class LastfmAuthenticationFrame extends React.Component{
// Open an authentication request window
var url = this.props.authorization_url+'?action=authorize'
var popup = window.open(url,"popup","height=500,width=350");
var popup = window.open(url,"popup","height=580,width=350");
popup.name = "LastfmAuthenticationWindow";
// Start timer to check our popup's state
var timer = setInterval(checkPopup, 1000);
@ -108,7 +109,7 @@ class LastfmAuthenticationFrame extends React.Component{
const mapStateToProps = (state, ownProps) => {
return {
authorization_url: state.lastfm.authorization_url,
authorized: state.lastfm.authorization,
authorized: state.lastfm.session,
authorizing: state.lastfm.authorizing
}
}

View File

@ -21,20 +21,20 @@ class SpotifyAuthenticationFrame extends React.Component{
}
componentDidMount(){
let self = this;
// Listen for incoming messages from the authorization iframe
// This is triggered when the popup posts a message, which is then passed to
// the iframe, and then passed on to the parent frame (our application)
// Listen for incoming messages from the authorization popup
window.addEventListener('message', function(event){
self.handleMessage(event)
var data = JSON.parse(event.data);
// Only digest messages relevant to us
if (data.origin == 'auth_spotify'){
self.handleMessage(event, data);
}
}, false);
}
handleMessage(event){
var data = JSON.parse(event.data)
handleMessage(event, data){
// Only allow incoming data from our authorized authenticator proxy
var authorization_domain = this.props.authorization_url.substring(0,this.props.authorization_url.indexOf('/',8))
@ -44,7 +44,7 @@ class SpotifyAuthenticationFrame extends React.Component{
}
// Spotify bounced with an error
if (typeof(data.error) !== 'undefined'){
if (data.error !== undefined){
this.props.uiActions.createNotification(data.error,'bad')
// No errors? We're in!
@ -80,7 +80,7 @@ class SpotifyAuthenticationFrame extends React.Component{
'playlist-read-collaborative',
'ugc-image-upload' // playlist image uploading
]
var popup = window.open(url+'&scope='+scopes.join('%20'),"popup","height=500,width=350");
var popup = window.open(url+'&scope='+scopes.join('%20'),"popup","height=580,width=350");
// Start timer to check our popup's state
var timer = setInterval(checkPopup, 1000);

View File

@ -97,6 +97,7 @@ const CoreMiddleware = (function(){
case 'CORE_START_SERVICES':
store.dispatch(mopidyActions.connect());
store.dispatch(pusherActions.connect());
store.dispatch(lastfmActions.connect());
next(action)
break

View File

@ -59,8 +59,11 @@ export function set(data){
**/
export function authorizationGranted(data){
data.token_expiry = new Date().getTime() + data.expires_in;
return { type: 'LASTFM_AUTHORIZATION_GRANTED', data: data }
data.session.expiry = new Date().getTime() + 3600;
return {
type: 'LASTFM_AUTHORIZATION_GRANTED',
data: data
}
}
export function revokeAuthorization(){
@ -70,12 +73,37 @@ export function revokeAuthorization(){
export function connect(){
return (dispatch, getState) => {
dispatch({ type: 'LASTFM_CONNECTING' })
dispatch({ type: 'LASTFM_CONNECTING' });
sendRequest(dispatch, getState, 'method=artist.getInfo&artist=')
// Authorized, dual-purpose our connection to get the current user
if (getState().lastfm.session){
dispatch(getMe());
// Not authorized, just use a generic lookup to test our connection
} else {
sendRequest(dispatch, getState, 'method=artist.getInfo&artist=')
.then(
response => {
dispatch({ type: 'LASTFM_CONNECTED' })
}
)
}
}
}
export function getMe(){
return (dispatch, getState) => {
var params = 'method=user.getInfo&user='+getState().lastfm.session.name
sendRequest(dispatch, getState, params)
.then(
response => {
dispatch({ type: 'LASTFM_CONNECTED' })
if (response.user){
dispatch({
type: 'LASTFM_USER_LOADED',
user: response.user
});
dispatch({ type: 'LASTFM_CONNECTED' })
}
}
)
}

View File

@ -0,0 +1,43 @@
import ReactGA from 'react-ga'
var helpers = require('./../../helpers')
var lastfmActions = require('./actions')
var uiActions = require('../ui/actions')
var pusherActions = require('../pusher/actions')
const LastfmMiddleware = (function(){
/**
* The actual middleware inteceptor
**/
return store => next => action => {
var state = store.getState();
switch(action.type){
case 'LASTFM_USER_LOADED':
var user = Object.assign(
{},
action.user,
{
uri: "lastfm:user:"+action.user.name
}
);
store.dispatch({
type: "USER_LOADED",
key: user.uri,
user: user
});
next(action);
break;
// This action is irrelevant to us, pass it on to the next middleware
default:
return next(action);
}
}
})();
export default LastfmMiddleware

View File

@ -13,22 +13,17 @@ export default function reducer(lastfm = {}, action){
return Object.assign({}, lastfm, action.data)
case 'LASTFM_AUTHORIZATION_GRANTED':
return Object.assign({}, lastfm, {
enabled: true,
return Object.assign({}, lastfm, {
authorizing: false,
authorization: action.data,
api_key: action.data.api_key,
token_expiry: action.data.token_expiry
session: action.data.session
})
case 'LASTFM_AUTHORIZATION_REVOKED':
return Object.assign({}, lastfm, {
authorizing: false,
authorization: false,
api_key: false,
token_expiry: 0,
authorizing: false,
session: false,
me: false
})
});
default:
return lastfm

View File

@ -167,6 +167,17 @@ const localstorageMiddleware = (function(){
);
localStorage.setItem('ui', JSON.stringify(ui))
break
case 'LASTFM_AUTHORIZATION_GRANTED':
var lastfm = JSON.parse(localStorage.getItem('lastfm') );
lastfm = Object.assign(
{},
{
session: action.data.session
}
);
localStorage.setItem('lastfm', JSON.stringify(lastfm));
break;
}
}

View File

@ -15,11 +15,13 @@ import Header from '../components/Header'
import Parallax from '../components/Parallax'
import Icon from '../components/Icon'
import Thumbnail from '../components/Thumbnail'
import URILink from '../components/URILink'
import * as coreActions from '../services/core/actions'
import * as uiActions from '../services/ui/actions'
import * as pusherActions from '../services/pusher/actions'
import * as mopidyActions from '../services/mopidy/actions'
import * as lastfmActions from '../services/lastfm/actions'
import * as spotifyActions from '../services/spotify/actions'
class Settings extends React.Component {
@ -109,21 +111,45 @@ class Settings extends React.Component {
if (user){
return (
<Link className="user" to={global.baseURL+'user/'+user.uri}>
<URILink className="user" type="user" uri={user.uri}>
<Thumbnail circle={true} size="small" images={user.images} />
<span className="user-name">
{user.display_name ? user.display_name : user.id}
</span>
</Link>
</URILink>
)
} else {
return (
<Link className="user">
<URILink className="user">
<Thumbnail circle={true} size="small" />
<span className="user-name">
Unknown
</span>
</Link>
</URILink>
)
}
}
renderLastfmUser(){
var user = this.props.core.users["lastfm:user:"+this.props.lastfm.session.name];
if (user){
return (
<URILink className="user" type="user" uri={user.uri}>
<Thumbnail circle={true} size="small" images={user.image} />
<span className="user-name">
{user.realname ? user.realname : user.name}
</span>
</URILink>
)
} else {
return (
<URILink className="user" type="user" uri={false}>
<Thumbnail circle={true} size="small" />
<span className="user-name">
Unknown
</span>
</URILink>
)
}
}
@ -302,6 +328,7 @@ class Settings extends React.Component {
</div>
</div>
</div>
<div className="field">
<div className="name">Authorization</div>
<div className="input">
@ -313,6 +340,15 @@ class Settings extends React.Component {
<h4 className="underline">LastFM</h4>
{this.props.lastfm.session ? <div className="field current-user">
<div className="name">Current user</div>
<div className="input">
<div className="text">
{ this.renderLastfmUser() }
</div>
</div>
</div> : null}
<div className="field">
<div className="name">Authorization</div>
<div className="input">
@ -408,6 +444,7 @@ const mapDispatchToProps = (dispatch) => {
uiActions: bindActionCreators(uiActions, dispatch),
pusherActions: bindActionCreators(pusherActions, dispatch),
mopidyActions: bindActionCreators(mopidyActions, dispatch),
lastfmActions: bindActionCreators(lastfmActions, dispatch),
spotifyActions: bindActionCreators(spotifyActions, dispatch)
}
}