integrated spotify auth/deauth, adding parts to playlist

This commit is contained in:
aj 2019-07-31 12:24:10 +01:00
parent 6617160c75
commit e3615d0ccf
10 changed files with 337 additions and 131 deletions

View File

@ -2,6 +2,8 @@ from flask import Blueprint, session, request, jsonify
from google.cloud import firestore from google.cloud import firestore
from werkzeug.security import check_password_hash, generate_password_hash from werkzeug.security import check_password_hash, generate_password_hash
import spotify.api.database as database
blueprint = Blueprint('api', __name__) blueprint = Blueprint('api', __name__)
db = firestore.Client() db = firestore.Client()
@ -11,15 +13,9 @@ def get_playlists():
if 'username' in session: if 'username' in session:
users = db.collection(u'spotify_users').where(u'username', u'==', session['username']).stream() pulled_user = database.get_user_doc_ref(session['username'])
users = [i for i in users] playlists = database.get_user_playlists_collection(pulled_user.id)
if len(users) == 1:
playlists = db.document(u'spotify_users/{}'.format(users[0].id)).collection(u'playlists')
else:
error = {'error': 'multiple usernames?'}
return jsonify(error), 500
response = { response = {
'playlists': [i.to_dict() for i in playlists.stream()] 'playlists': [i.to_dict() for i in playlists.stream()]
@ -28,8 +24,7 @@ def get_playlists():
return jsonify(response), 200 return jsonify(response), 200
else: else:
error = {'error': 'not logged in'} return jsonify({'error': 'not logged in'}), 401
return jsonify(error), 401
@blueprint.route('/playlist', methods=['GET', 'POST', 'PUT']) @blueprint.route('/playlist', methods=['GET', 'POST', 'PUT'])
@ -37,33 +32,25 @@ def get_playlist():
if 'username' in session: if 'username' in session:
users = db.collection(u'spotify_users').where(u'username', u'==', session['username']).stream() user_ref = database.get_user_doc_ref(session['username'])
playlists = database.get_user_playlists_collection(user_ref.id)
users = [i for i in users]
if len(users) == 1:
playlists = db.document(u'spotify_users/{}'.format(users[0].id)).collection(u'playlists')
else:
error = {'error': 'multiple usernames?'}
return jsonify(error), 500
if request.method == 'GET': if request.method == 'GET':
playlist_name = request.args.get('name', None) playlist_name = request.args.get('name', None)
if playlist_name: if playlist_name:
playlist = [i for i in playlists.where(u'name', u'==', playlist_name).stream()] queried_playlist = [i for i in playlists.where(u'name', u'==', playlist_name).stream()]
if len(playlist) == 0: if len(queried_playlist) == 0:
return jsonify({'error': 'no playlist found'}), 404 return jsonify({'error': 'no playlist found'}), 404
elif len(playlist) > 1: elif len(queried_playlist) > 1:
return jsonify({'error': 'multiple playlists found'}), 500 return jsonify({'error': 'multiple playlists found'}), 500
return jsonify(playlist[0].to_dict()), 200 return jsonify(queried_playlist[0].to_dict()), 200
else: else:
response = {"error": 'no name requested'} return jsonify({"error": 'no name requested'}), 400
return jsonify(response), 400
elif request.method == 'POST' or request.method == 'PUT': elif request.method == 'POST' or request.method == 'PUT':
@ -73,12 +60,54 @@ def get_playlist():
return jsonify({'error': "no name provided"}), 400 return jsonify({'error': "no name provided"}), 400
playlist_name = request_json['name'] playlist_name = request_json['name']
playlist_parts = request_json.get('parts', None)
playlist_id = request_json.get('id', None)
return 404 queried_playlist = [i for i in playlists.where(u'name', u'==', playlist_name).stream()]
if request.method == 'PUT':
if len(queried_playlist) != 0:
return jsonify({'error': 'playlist already exists'}), 400
if playlist_parts is None or playlist_id is None:
return jsonify({'error': 'parts and id required'}), 400
playlists.add({
'name': playlist_name,
'parts': playlist_parts,
'playlist_id': playlist_id
})
return jsonify({"message": 'playlist added', "status": "success"}), 200
else:
if len(queried_playlist) == 0:
return jsonify({'error': "playlist doesn't exist"}), 400
if len(queried_playlist) > 1:
return jsonify({'error': "multiple playlists exist"}), 500
if playlist_parts is None and playlist_id is None:
return jsonify({'error': "no chnages to make"}), 400
playlist_doc = playlists.document(queried_playlist[0].id)
dic = {}
if playlist_parts:
dic['parts'] = playlist_parts
if playlist_id:
dic['playlist_id'] = playlist_id
playlist_doc.update(dic)
return jsonify({"message": 'playlist updated', "status": "success"}), 200
else: else:
error = {'error': 'not logged in'} return jsonify({'error': 'not logged in'}), 401
return jsonify(error), 401
@blueprint.route('/user', methods=['GET']) @blueprint.route('/user', methods=['GET'])
@ -86,28 +115,19 @@ def user():
if 'username' in session: if 'username' in session:
users = db.collection(u'spotify_users').where(u'username', u'==', session['username']).stream() pulled_user = database.get_user_doc_ref(session['username']).get().to_dict()
users = [i for i in users]
if len(users) == 1:
pulled_user = db.collection(u'spotify_users').document(u'{}'.format(users[0].id)).get()
else:
error = {'error': 'multiple usernames?'}
return jsonify(error), 500
doc = pulled_user.to_dict()
response = { response = {
'username': doc['username'], 'username': pulled_user['username'],
'type': doc['type'], 'type': pulled_user['type'],
'validated': doc['validated'] 'spotify_linked': pulled_user['spotify_linked'],
'validated': pulled_user['validated']
} }
return jsonify(response), 200 return jsonify(response), 200
else: else:
error = {'error': 'not logged in'} return jsonify({'error': 'not logged in'}), 401
return jsonify(error), 401
@blueprint.route('/user/password', methods=['POST']) @blueprint.route('/user/password', methods=['POST'])
@ -120,21 +140,12 @@ def change_password():
if 'new_password' in request_json and 'current_password' in request_json: if 'new_password' in request_json and 'current_password' in request_json:
if len(request_json['new_password']) == 0: if len(request_json['new_password']) == 0:
response = {"error": 'zero length password'} return jsonify({"error": 'zero length password'}), 400
return jsonify(response), 400
if len(request_json['new_password']) > 30: if len(request_json['new_password']) > 30:
response = {"error": 'password too long'} return jsonify({"error": 'password too long'}), 400
return jsonify(response), 400
users = db.collection(u'spotify_users').where(u'username', u'==', session['username']).stream() current_user = database.get_user_doc_ref(session['username'])
users = [i for i in users]
if len(users) == 1:
current_user = db.collection(u'spotify_users').document(u'{}'.format(users[0].id))
else:
error = {'error': 'multiple usernames?'}
return jsonify(error), 500
if check_password_hash(current_user.get().to_dict()['password'], request_json['current_password']): if check_password_hash(current_user.get().to_dict()['password'], request_json['current_password']):
@ -144,18 +155,10 @@ def change_password():
return jsonify(response), 200 return jsonify(response), 200
else: else:
error = {'error': 'wrong password provided'} return jsonify({'error': 'wrong password provided'}), 403
return jsonify(error), 403
else: else:
error = {'error': 'malformed request, no old_password/new_password'} return jsonify({'error': 'malformed request, no old_password/new_password'}), 400
return jsonify(error), 400
else: else:
error = {'error': 'not logged in'} return jsonify({'error': 'not logged in'}), 401
return jsonify(error), 401
@blueprint.route('/playlist', methods=['GET', 'PUT', 'POST'])
def playlist():
return 401

32
spotify/api/database.py Normal file
View File

@ -0,0 +1,32 @@
from google.cloud import firestore
db = firestore.Client()
def get_user_query_stream(user):
users = db.collection(u'spotify_users').where(u'username', u'==', user).stream()
users = [i for i in users]
return users
def get_user_doc_ref(user):
users = get_user_query_stream(user)
if len(users) == 1:
return db.collection(u'spotify_users').document(u'{}'.format(users[0].id))
else:
print(len(users))
raise ValueError
def get_user_playlists_collection(user_id):
playlists = db.document(u'spotify_users/{}'.format(user_id)).collection(u'playlists')
return playlists

View File

@ -2,6 +2,13 @@ from flask import Blueprint, session, flash, request, redirect, url_for
from google.cloud import firestore from google.cloud import firestore
from werkzeug.security import check_password_hash from werkzeug.security import check_password_hash
import urllib
import datetime
from base64 import b64encode
import requests
import spotify.api.database as database
blueprint = Blueprint('authapi', __name__) blueprint = Blueprint('authapi', __name__)
db = firestore.Client() db = firestore.Client()
@ -17,9 +24,7 @@ def login():
username = request.form['username'].lower() username = request.form['username'].lower()
password = request.form['password'] password = request.form['password']
users = db.collection(u'spotify_users').where(u'username', u'==', username).stream() users = database.get_user_query_stream(username)
users = [i for i in users]
if len(users) == 0: if len(users) == 0:
flash('user not found') flash('user not found')
@ -35,6 +40,10 @@ def login():
return redirect(url_for('index')) return redirect(url_for('index'))
if check_password_hash(doc['password'], password): if check_password_hash(doc['password'], password):
user_reference = db.collection(u'spotify_users').document(u'{}'.format(users[0].id))
user_reference.update({'last_login': datetime.datetime.utcnow()})
session['username'] = username session['username'] = username
return redirect(url_for('app_route')) return redirect(url_for('app_route'))
else: else:
@ -50,3 +59,79 @@ def logout():
session.pop('username', None) session.pop('username', None)
flash('logged out') flash('logged out')
return redirect(url_for('index')) return redirect(url_for('index'))
@blueprint.route('/spotify')
def auth():
if 'username' in session:
client_id = db.document('key/spotify').get().to_dict()['clientid']
params = urllib.parse.urlencode(
{
'client_id': client_id,
'response_type': 'code',
'scope': 'playlist-modify-public playlist-modify-private playlist-read-private',
'redirect_uri': 'https://spotify.sarsoo.xyz/auth/spotify/token'
}
)
return redirect(urllib.parse.urlunparse(['https', 'accounts.spotify.com', 'authorize', '', params, '']))
return redirect('/')
@blueprint.route('/spotify/token')
def token():
if 'username' in session:
code = request.args.get('code', None)
if code is None:
error = request.args.get('error', None)
print('error')
else:
app_credentials = db.document('key/spotify').get().to_dict()
idsecret = b64encode(bytes(app_credentials['clientid'] + ':' + app_credentials['clientsecret'], "utf-8")).decode("ascii")
headers = {'Authorization': 'Basic %s' % idsecret}
data = {
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': 'https://spotify.sarsoo.xyz/auth/spotify/token'
}
req = requests.post('https://accounts.spotify.com/api/token', data=data, headers=headers)
resp = req.json()
user_reference = database.get_user_doc_ref(session['username'])
user_reference.update({
'access_token': resp['access_token'],
'refresh_token': resp['refresh_token'],
'spotify_linked': True
})
return redirect('/app/settings/spotify')
return redirect('/')
@blueprint.route('/spotify/deauth')
def deauth():
if 'username' in session:
user_reference = database.get_user_doc_ref(session['username'])
user_reference.update({
'access_token': None,
'refresh_token': None,
'spotify_linked': False
})
return redirect('/app/settings/spotify')
return redirect('/')

View File

@ -1,11 +1,7 @@
from flask import Flask, render_template, redirect, request, session, flash, url_for from flask import Flask, render_template, redirect, request, session, flash, url_for
from google.cloud import firestore from google.cloud import firestore
import requests
from base64 import b64encode
import os import os
import urllib
from spotify.auth import auth_blueprint from spotify.auth import auth_blueprint
from spotify.api import api_blueprint from spotify.api import api_blueprint
@ -24,51 +20,6 @@ def index():
return render_template('index.html') return render_template('index.html')
@app.route('/spotify/auth')
def auth():
client_id = db.document('key/spotify').get().to_dict()['clientid']
params = urllib.parse.urlencode(
{
'client_id': client_id,
'response_type': 'code',
'scope': 'playlist-modify-public playlist-modify-private playlist-read-private',
'redirect_uri': 'https://spotify.sarsoo.xyz/token'
}
)
return redirect(urllib.parse.urlunparse(['https', 'accounts.spotify.com', 'authorize', '', params, '']))
@app.route('/token')
def token():
code = request.args.get('code', None)
if code is None:
error = request.args.get('error', None)
print('error')
else:
app_credentials = db.document('key/spotify').get().to_dict()
idsecret = b64encode(bytes(app_credentials['clientid'] + ':' + app_credentials['clientsecret'], "utf-8")).decode("ascii")
headers = {'Authorization': 'Basic %s' % idsecret}
data = {
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': 'https://spotify.sarsoo.xyz/token'
}
req = requests.post('https://accounts.spotify.com/api/token', data=data, headers=headers)
resp = req.json()
# print(str(req.status_code) + str(resp))
# if 200 <= req.status_code < 300:
return redirect('/app')
@app.route('/app', defaults={'path': ''}) @app.route('/app', defaults={'path': ''})
@app.route('/app/<path:path>') @app.route('/app/<path:path>')
def app_route(path): def app_route(path):

View File

@ -22,7 +22,7 @@ class Index extends Component{
} }
render(){ render(){
return <p>index</p>; return <h1 className="center-text">welcome to playlist manager!</h1>;
} }
} }

View File

@ -1,21 +1,97 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
const axios = require('axios'); const axios = require('axios');
class PlaylistView extends Component{ class PlaylistView extends Component{
constructor(props){ constructor(props){
super(props); super(props);
console.log(this.props); console.log(this.props.match.params.name);
this.state = { this.state = {
name: this.props.match.name name: this.props.match.params.name,
parts: [],
error: false,
error_text: null,
add_part_value: ''
} }
this.handleAddPart = this.handleAddPart.bind(this);
this.handleAddPartChange = this.handleAddPartChange.bind(this);
}
componentDidMount(){
this.getPlaylistInfo();
}
getPlaylistInfo(){
axios.get(`/api/playlist?name=${ this.state.name }`)
.then((response) => {
this.setState(response.data);
}).catch((error) => {
this.setState({
error: true,
error_text: "error pulling playlist info"
});
});
}
handleAddPartChange(event){
this.setState({
add_part_value: event.target.value
});
}
handleAddPart(event){
var parts = this.state.parts;
parts.push(this.state.add_part_value);
this.setState({
parts: parts
});
axios.post('/api/playlist', {
name: this.state.name,
parts: parts
}).then((response) => {
console.log(reponse);
}).catch((error) => {
console.log(error);
});
} }
render(){ render(){
return <p>{this.state.name}</p>;
const table = (
<table className="app-table max-width">
<thead>
<tr>
<th colSpan="2"><h1>{ this.state.name }</h1></th>
</tr>
</thead>
<tbody>
{ this.state.parts.map((part) => <Row part={ part } key={ part }/>) }
<tr>
<td>
<input type="text" className="full-width" value={this.state.add_part_value} onChange={this.handleAddPartChange}></input>
</td>
<td>
<button className="button full-width" onClick={this.handleAddPart}>add</button>
</td>
</tr>
</tbody>
</table>
);
const error = <p style={{textAlign: "center"}}>{ this.state.error_text }</p>;
return this.state.error ? error : table;
} }
} }
export default PlaylistView function Row (props) {
return (
<tr>
<td className="ui-text center-text">{ props.part }</td>
<td className="ui-text center-text">remove</td>
</tr>
);
}
export default PlaylistView;

View File

@ -16,7 +16,8 @@ class PlaylistManager extends Component {
constructor(props){ constructor(props){
super(props); super(props);
this.state = { this.state = {
type: null type: null,
spotify_linked: null
} }
} }
@ -37,7 +38,8 @@ class PlaylistManager extends Component {
}) })
.then((response) => { .then((response) => {
self.setState({ self.setState({
type: response.data.type type: response.data.type,
spotify_linked: response.data.spotify_linked
}) })
}); });
} }
@ -61,7 +63,7 @@ class PlaylistManager extends Component {
<Switch> <Switch>
<Route path="/app" exact component={Index} /> <Route path="/app" exact component={Index} />
<Route path="/app/playlists" exact component={Playlists} /> <Route path="/app/playlists" exact component={Playlists} />
<Route path="/app/settings" component={Settings} /> <Route path="/app/settings" render={(props) => <Settings {...props} spotify_linked={this.state.spotify_linked}/>} />
{ this.state.type == 'admin' && <Route path="/app/admin" component={Admin} /> } { this.state.type == 'admin' && <Route path="/app/admin" component={Admin} /> }
<Route path='/app/playlist/:name' component={PlaylistView} /> <Route path='/app/playlist/:name' component={PlaylistView} />
<Route component={NotFound} /> <Route component={NotFound} />

View File

@ -80,9 +80,13 @@ class ChangePassword extends Component {
render(){ render(){
return ( return (
<div> <div>
<h1>change password</h1>
<form onSubmit={this.handleSubmit}> <form onSubmit={this.handleSubmit}>
<table className="app-table max-width"> <table className="app-table max-width">
<thead>
<tr>
<th colSpan="2"><h1>change password</h1></th>
</tr>
</thead>
<tbody> <tbody>
<tr> <tr>
<td className="ui-text center-text">current:</td> <td className="ui-text center-text">current:</td>
@ -117,7 +121,7 @@ class ChangePassword extends Component {
</tbody> </tbody>
</table> </table>
</form> </form>
{ this.state.error && <p style={{color: "red"}}>{this.state.errorValue}</p>} { this.state.error && <p style={{color: "red"}} className="center-text">{this.state.errorValue}</p>}
</div> </div>
); );
} }

View File

@ -1,7 +1,8 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { BrowserRouter as Router, Route, Link, Switch, Redirect} from "react-router-dom"; import { BrowserRouter as Router, Route, Link, Switch, Redirect} from "react-router-dom";
import ChangePassword from "./ChangePassword.js" import ChangePassword from "./ChangePassword.js";
import SpotifyLink from "./SpotifyLink.js";
class Settings extends Component { class Settings extends Component {
@ -10,9 +11,11 @@ class Settings extends Component {
<div> <div>
<ul className="navbar" style={{width: "100%"}}> <ul className="navbar" style={{width: "100%"}}>
<li><Link to={`${this.props.match.url}/password`}>password</Link></li> <li><Link to={`${this.props.match.url}/password`}>password</Link></li>
<li><Link to={`${this.props.match.url}/spotify`}>spotify</Link></li>
</ul> </ul>
<Route path={`${this.props.match.url}/password`} component={ChangePassword} /> <Route path={`${this.props.match.url}/password`} component={ChangePassword} />
<Route path={`${this.props.match.url}/spotify`} render={(props) => <SpotifyLink {...props} spotify_linked={this.props.spotify_linked}/>} />
</div> </div>
); );

View File

@ -0,0 +1,50 @@
import React, { Component } from "react";
const axios = require('axios');
class SpotifyLink extends Component {
constructor(props){
super(props);
this.state = {
spotify_linked: props.spotify_linked
}
}
getUserInfo(){
}
render(){
return (
<table className="app-table max-width">
<thead>
<tr>
<th><h1 className="ui-text center-text">spotify link status</h1></th>
</tr>
</thead>
<tbody>
<tr>
<td className="ui-text center-text">
status: { this.state.spotify_linked ? "linked" : "unlinked" }
</td>
</tr>
<tr>
<td>
{ this.state.spotify_linked ? <DeAuthButton /> : <AuthButton /> }
</td>
</tr>
</tbody>
</table>
);
}
}
function AuthButton(props) {
return <a className="button full-width" href="/auth/spotify">auth</a>;
}
function DeAuthButton(props) {
return <a className="button full-width" href="/auth/spotify/deauth">de-auth</a>;
}
export default SpotifyLink;