added basic auth api support, added device name specification in play

This commit is contained in:
aj 2019-09-25 19:28:38 +01:00
parent 5909229d69
commit 215c839210
4 changed files with 131 additions and 72 deletions

View File

@ -25,20 +25,50 @@ task_path = tasker.queue_path('sarsooxyz', 'europe-west2', 'spotify-executions')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def is_logged_in():
if 'username' in session:
return True
else:
return False
def is_basic_authed():
if request.authorization:
if request.authorization.get('username', None) and request.authorization.get('password', None):
if database.check_user_password(request.authorization.username, request.authorization.password):
return True
return False
def login_required(func): def login_required(func):
@functools.wraps(func) @functools.wraps(func)
def wrapper(*args, **kwargs): def login_required_wrapper(*args, **kwargs):
if 'username' in session: if is_logged_in():
return func(*args, **kwargs) return func(username=session['username'], *args, **kwargs)
else: else:
logger.warning('user not logged in') logger.warning('user not logged in')
return jsonify({'error': 'not logged in'}), 401 return jsonify({'error': 'not logged in'}), 401
return wrapper return login_required_wrapper
def login_or_basic_auth(func):
@functools.wraps(func)
def login_or_basic_auth_wrapper(*args, **kwargs):
if is_logged_in():
return func(username=session['username'], *args, **kwargs)
elif is_basic_authed():
return func(username=request.authorization.username, *args, **kwargs)
else:
logger.warning('user not logged in')
return jsonify({'error': 'not logged in'}), 401
return login_or_basic_auth_wrapper
def admin_required(func): def admin_required(func):
@functools.wraps(func) @functools.wraps(func)
def wrapper(*args, **kwargs): def admin_required_wrapper(*args, **kwargs):
user_dict = database.get_user_doc_ref(session['username']).get().to_dict() user_dict = database.get_user_doc_ref(session['username']).get().to_dict()
if user_dict: if user_dict:
@ -51,12 +81,12 @@ def admin_required(func):
logger.warning('user not logged in') logger.warning('user not logged in')
return jsonify({'error': 'not logged in'}), 401 return jsonify({'error': 'not logged in'}), 401
return wrapper return admin_required_wrapper
def gae_cron(func): def gae_cron(func):
@functools.wraps(func) @functools.wraps(func)
def wrapper(*args, **kwargs): def gae_cron_wrapper(*args, **kwargs):
if request.headers.get('X-Appengine-Cron', None): if request.headers.get('X-Appengine-Cron', None):
return func(*args, **kwargs) return func(*args, **kwargs)
@ -64,12 +94,12 @@ def gae_cron(func):
logger.warning('user not logged in') logger.warning('user not logged in')
return jsonify({'status': 'error', 'message': 'unauthorised'}), 401 return jsonify({'status': 'error', 'message': 'unauthorised'}), 401
return wrapper return gae_cron_wrapper
def cloud_task(func): def cloud_task(func):
@functools.wraps(func) @functools.wraps(func)
def wrapper(*args, **kwargs): def cloud_task_wrapper(*args, **kwargs):
if request.headers.get('X-AppEngine-QueueName', None): if request.headers.get('X-AppEngine-QueueName', None):
return func(*args, **kwargs) return func(*args, **kwargs)
@ -77,14 +107,14 @@ def cloud_task(func):
logger.warning('non tasks request') logger.warning('non tasks request')
return jsonify({'status': 'error', 'message': 'unauthorised'}), 401 return jsonify({'status': 'error', 'message': 'unauthorised'}), 401
return wrapper return cloud_task_wrapper
@blueprint.route('/playlists', methods=['GET']) @blueprint.route('/playlists', methods=['GET'])
@login_required @login_or_basic_auth
def get_playlists(): def get_playlists(username=None):
pulled_user = database.get_user_doc_ref(session['username']) pulled_user = database.get_user_doc_ref(username)
playlists = pulled_user.collection(u'playlists') playlists = pulled_user.collection(u'playlists')
@ -102,10 +132,10 @@ def get_playlists():
@blueprint.route('/playlist', methods=['GET', 'POST', 'PUT', 'DELETE']) @blueprint.route('/playlist', methods=['GET', 'POST', 'PUT', 'DELETE'])
@login_required @login_or_basic_auth
def playlist(): def playlist(username=None):
user_ref = database.get_user_doc_ref(session['username']) user_ref = database.get_user_doc_ref(username)
playlists = user_ref.collection(u'playlists') playlists = user_ref.collection(u'playlists')
if request.method == 'GET' or request.method == 'DELETE': if request.method == 'GET' or request.method == 'DELETE':
@ -131,7 +161,7 @@ def playlist():
elif request.method == 'DELETE': elif request.method == 'DELETE':
logger.info(f'deleted {session["username"]} / {queried_playlist[0].to_dict()["name"]}') logger.info(f'deleted {username} / {queried_playlist[0].to_dict()["name"]}')
queried_playlist[0].reference.delete() queried_playlist[0].reference.delete()
return jsonify({"message": 'playlist deleted', "status": "success"}), 200 return jsonify({"message": 'playlist deleted', "status": "success"}), 200
@ -199,7 +229,7 @@ def playlist():
} }
if user_ref.get().to_dict()['spotify_linked']: if user_ref.get().to_dict()['spotify_linked']:
new_playlist = create_playlist(session['username'], playlist_name) new_playlist = create_playlist(username, playlist_name)
to_add['uri'] = str(new_playlist.uri) if new_playlist is not None else None to_add['uri'] = str(new_playlist.uri) if new_playlist is not None else None
if playlist_type == 'recents': if playlist_type == 'recents':
@ -208,7 +238,7 @@ def playlist():
to_add['add_last_month'] = playlist_add_last_month if playlist_add_last_month is not None else False to_add['add_last_month'] = playlist_add_last_month if playlist_add_last_month is not None else False
playlists.document().set(to_add) playlists.document().set(to_add)
logger.info(f'added {session["username"]} / {playlist_name}') logger.info(f'added {username} / {playlist_name}')
return jsonify({"message": 'playlist added', "status": "success"}), 201 return jsonify({"message": 'playlist added', "status": "success"}), 201
@ -261,22 +291,22 @@ def playlist():
dic['type'] = playlist_type dic['type'] = playlist_type
if len(dic) == 0: if len(dic) == 0:
logger.warning(f'no changes to make for {session["username"]} / {playlist_name}') logger.warning(f'no changes to make for {username} / {playlist_name}')
return jsonify({"message": 'no changes to make', "status": "error"}), 400 return jsonify({"message": 'no changes to make', "status": "error"}), 400
playlist_doc.update(dic) playlist_doc.update(dic)
logger.info(f'updated {session["username"]} / {playlist_name}') logger.info(f'updated {username} / {playlist_name}')
return jsonify({"message": 'playlist updated', "status": "success"}), 200 return jsonify({"message": 'playlist updated', "status": "success"}), 200
@blueprint.route('/user', methods=['GET', 'POST']) @blueprint.route('/user', methods=['GET', 'POST'])
@login_required @login_or_basic_auth
def user(): def user(username=None):
if request.method == 'GET': if request.method == 'GET':
pulled_user = database.get_user_doc_ref(session['username']).get().to_dict() pulled_user = database.get_user_doc_ref(username).get().to_dict()
response = { response = {
'username': pulled_user['username'], 'username': pulled_user['username'],
@ -289,7 +319,7 @@ def user():
else: else:
if database.get_user_doc_ref(session['username']).get().to_dict()['type'] != 'admin': if database.get_user_doc_ref(username).get().to_dict()['type'] != 'admin':
return jsonify({'status': 'error', 'message': 'unauthorized'}), 401 return jsonify({'status': 'error', 'message': 'unauthorized'}), 401
request_json = request.get_json() request_json = request.get_json()
@ -383,8 +413,8 @@ def change_password():
@blueprint.route('/playlist/play', methods=['POST']) @blueprint.route('/playlist/play', methods=['POST'])
@login_required @login_or_basic_auth
def play_playlist(): def play_playlist(username=None):
request_json = request.get_json() request_json = request.get_json()
@ -398,42 +428,40 @@ def play_playlist():
request_add_this_month = request_json.get('add_this_month', False) request_add_this_month = request_json.get('add_this_month', False)
request_add_last_month = request_json.get('add_last_month', False) request_add_last_month = request_json.get('add_last_month', False)
logger.info(f'playing {session["username"]}') request_device_name = request_json.get('device_name', None)
if request_parts or request_playlists: logger.info(f'playing {username}')
if len(request_parts) > 0 or len(request_playlists) > 0:
if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': if (request_parts and len(request_parts) > 0) or (request_playlists and len(request_playlists) > 0):
create_play_user_playlist_task(session['username'],
parts=request_parts,
playlist_type=request_playlist_type,
playlists=request_playlists,
shuffle=request_shuffle,
include_recommendations=request_include_recommendations,
recommendation_sample=request_recommendation_sample,
day_boundary=request_day_boundary,
add_this_month=request_add_this_month,
add_last_month=request_add_last_month)
else:
play_user_playlist(session['username'],
parts=request_parts,
playlist_type=request_playlist_type,
playlists=request_playlists,
shuffle=request_shuffle,
include_recommendations=request_include_recommendations,
recommendation_sample=request_recommendation_sample,
day_boundary=request_day_boundary,
add_this_month=request_add_this_month,
add_last_month=request_add_last_month)
return jsonify({'message': 'execution requested', 'status': 'success'}), 200
if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD':
create_play_user_playlist_task(username,
parts=request_parts,
playlist_type=request_playlist_type,
playlists=request_playlists,
shuffle=request_shuffle,
include_recommendations=request_include_recommendations,
recommendation_sample=request_recommendation_sample,
day_boundary=request_day_boundary,
add_this_month=request_add_this_month,
add_last_month=request_add_last_month,
device_name=request_device_name)
else: else:
logger.error(f'insufficient playlist/part lengths {session["username"]}') play_user_playlist(username,
return jsonify({'error': 'insufficient playlist sources'}), 400 parts=request_parts,
playlist_type=request_playlist_type,
playlists=request_playlists,
shuffle=request_shuffle,
include_recommendations=request_include_recommendations,
recommendation_sample=request_recommendation_sample,
day_boundary=request_day_boundary,
add_this_month=request_add_this_month,
add_last_month=request_add_last_month,
device_name=request_device_name)
return jsonify({'message': 'execution requested', 'status': 'success'}), 200
else: else:
logger.error(f'no playlists/parts {session["username"]}') logger.error(f'no playlists/parts {username}')
return jsonify({'error': 'insufficient playlist sources'}), 400 return jsonify({'error': 'insufficient playlist sources'}), 400
@ -454,23 +482,24 @@ def play_playlist_task():
recommendation_sample=payload['recommendation_sample'], recommendation_sample=payload['recommendation_sample'],
day_boundary=payload['day_boundary'], day_boundary=payload['day_boundary'],
add_this_month=payload['add_this_month'], add_this_month=payload['add_this_month'],
add_last_month=payload['add_last_month']) add_last_month=payload['add_last_month'],
device_name=payload['device_name'])
return jsonify({'message': 'executed playlist', 'status': 'success'}), 200 return jsonify({'message': 'executed playlist', 'status': 'success'}), 200
@blueprint.route('/playlist/run', methods=['GET']) @blueprint.route('/playlist/run', methods=['GET'])
@login_required @login_or_basic_auth
def run_playlist(): def run_playlist(username=None):
playlist_name = request.args.get('name', None) playlist_name = request.args.get('name', None)
if playlist_name: if playlist_name:
if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD': if os.environ.get('DEPLOY_DESTINATION', None) == 'PROD':
create_run_user_playlist_task(session['username'], playlist_name) create_run_user_playlist_task(username, playlist_name)
else: else:
run_user_playlist(session['username'], playlist_name) run_user_playlist(username, playlist_name)
return jsonify({'message': 'execution requested', 'status': 'success'}), 200 return jsonify({'message': 'execution requested', 'status': 'success'}), 200
@ -495,13 +524,13 @@ def run_playlist_task():
@blueprint.route('/playlist/run/user', methods=['GET']) @blueprint.route('/playlist/run/user', methods=['GET'])
@login_required @login_or_basic_auth
def run_user(): def run_user(username=None):
if database.get_user_doc_ref(session['username']).get().to_dict()['type'] == 'admin': if database.get_user_doc_ref(username).get().to_dict()['type'] == 'admin':
user_name = request.args.get('username', session['username']) user_name = request.args.get('username', username)
else: else:
user_name = session['username'] user_name = username
execute_user(user_name) execute_user(user_name)
@ -623,7 +652,8 @@ def create_play_user_playlist_task(username,
day_boundary=10, day_boundary=10,
add_this_month=False, add_this_month=False,
add_last_month=False, add_last_month=False,
delay=0): delay=0,
device_name=None):
task = { task = {
'app_engine_http_request': { # Specify the type of request. 'app_engine_http_request': { # Specify the type of request.
'http_method': 'POST', 'http_method': 'POST',
@ -638,7 +668,8 @@ def create_play_user_playlist_task(username,
'recommendation_sample': recommendation_sample, 'recommendation_sample': recommendation_sample,
'day_boundary': day_boundary, 'day_boundary': day_boundary,
'add_this_month': add_this_month, 'add_this_month': add_this_month,
'add_last_month': add_last_month 'add_last_month': add_last_month,
'device_name': device_name
}).encode() }).encode()
} }
} }

View File

@ -136,7 +136,7 @@ def auth():
{ {
'client_id': client_id, 'client_id': client_id,
'response_type': 'code', 'response_type': 'code',
'scope': 'playlist-modify-public playlist-modify-private playlist-read-private user-modify-playback-state user-library-read', 'scope': 'playlist-modify-public playlist-modify-private playlist-read-private user-read-playback-state user-modify-playback-state user-library-read',
'redirect_uri': 'https://spotify.sarsoo.xyz/auth/spotify/token' 'redirect_uri': 'https://spotify.sarsoo.xyz/auth/spotify/token'
} }
) )

View File

@ -1,12 +1,29 @@
from google.cloud import firestore from google.cloud import firestore
import logging import logging
from typing import List, Optional from typing import List, Optional
from werkzeug.security import check_password_hash
db = firestore.Client() db = firestore.Client()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def check_user_password(username, password):
user = get_user_doc_ref(user=username)
if user:
user_dict = user.get().to_dict()
if check_password_hash(user_dict['password'], password):
return True
else:
logger.error(f'password mismatch {username}')
else:
logger.error(f'user {username} not found')
return False
def get_user_query_stream(user: str) -> List[firestore.DocumentSnapshot]: def get_user_query_stream(user: str) -> List[firestore.DocumentSnapshot]:
users = [i for i in db.collection(u'spotify_users').where(u'username', u'==', user).stream()] users = [i for i in db.collection(u'spotify_users').where(u'username', u'==', user).stream()]

View File

@ -28,7 +28,8 @@ def play_user_playlist(username,
recommendation_sample=10, recommendation_sample=10,
day_boundary=10, day_boundary=10,
add_this_month=False, add_this_month=False,
add_last_month=False): add_last_month=False,
device_name=None):
users = database.get_user_query_stream(username) users = database.get_user_query_stream(username)
@ -59,6 +60,16 @@ def play_user_playlist(username,
user_dict['refresh_token'], user_dict['refresh_token'],
user_dict['access_token'])) user_dict['access_token']))
device = None
if device_name:
devices = net.get_available_devices()
if devices and len(devices) > 0:
device = next((i for i in devices if i.name == device_name), None)
if device is None:
logger.error(f'error selecting device {device_name} to play on')
else:
logger.warning(f'no available devices to play')
engine = PlaylistEngine(net) engine = PlaylistEngine(net)
player = Player(net) player = Player(net)
@ -95,7 +106,7 @@ def play_user_playlist(username,
else: else:
tracks = engine.make_playlist(params=params) tracks = engine.make_playlist(params=params)
player.play(tracks=tracks) player.play(tracks=tracks, device=device)
else: else:
logger.critical(f'multiple/no user(s) found ({username})') logger.critical(f'multiple/no user(s) found ({username})')