async def register_user(first_name: str, last_name: str, email: str, password: str, config: Config): if not check_password(password): raise err.bad_request( "Please provide a password at least 8 characters long, containing letters and symbols" ) password_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()) try: async with config.get_database_connection() as db: user_persistence = users.UsersPersistence(db) created_user_id = await user_persistence.create_user( first_name, last_name, email, password_hash) user = await user_persistence.get_user_by_id(created_user_id) except IntegrityError as e: logger.debug("IntegrityError registering user with email %s: %s", email, e) raise err.bad_request("Email address is already in use") except Exception as e: logger.exception("Failed to get user with email %s from db: %s", email, e) raise err.internal_server_error() return { "token": jwt.generate_jwt(user["user_id"], jwt.Aud.AUTH, config.jwt_key()) }
async def login(email: str, password: str, config: Config) -> Dict: """ Get user from DB, check password hash matches and return a JWT token if so """ try: async with config.get_database_connection() as db: user = await users.UsersPersistence(db).get_user_by_email(email) except Exception as e: logger.exception("Failed to get user with email %s from db: %s", email, e) raise err.internal_server_error() auth_failed = err.unauthorized( "Failed to log you in with the provided credentials; please try again") if not user: logger.debug("User not found with email %s", email) raise auth_failed if not bcrypt.checkpw(password.encode(), user["password_hash"]): logger.debug("Incorrect password for user(id=%s)", user["user_id"]) raise auth_failed return { "token": jwt.generate_jwt(user["user_id"], jwt.Aud.AUTH, config.jwt_key()) }
async def get_room_by_id_minimal(room_id: int, user_id: int, config: Config) -> Dict: """ Get data for a room by id, redacting secret information like the room code """ try: async with config.get_database_connection() as db: room_persistence = rooms.RoomPersistence(db) room = await room_persistence.get_room(room_id) if not room or not room.get("active"): raise err.not_found(f"No active room with id {room_id}") user_in_room = await is_user_in_room(user_id, room_id, db, room=room) return { "room_id": room.get("room_id"), "owner_id": room.get("owner_id"), "room_name": room.get("room_name"), "has_code": bool(room.get("room_code")), "user_in_room": user_in_room } except Exception as e: logger.exception("Failed to retrieve minimal data for room %s: %s", room_id, e) raise e
async def join_room(user_id: int, room_id: int, room_code: Optional[str], config: Config) -> Dict: """Process a request from a user to join a room""" try: async with config.get_database_connection() as db: room_persistence = rooms.RoomPersistence(db) room = await room_persistence.get_room(room_id) if not room or not room.get("active"): raise err.not_found(f"Active room with id {room_id} not found") if await is_user_in_room(user_id, room_id, db, room=room): return {"success": True, "message": "User is already in room"} if room.get("room_code"): if room["room_code"] != room_code: return {"success": False, "message": "Invalid room code"} await room_persistence.add_user_to_room(room_id, user_id) return {"success": True, "message": "Successfully joined the room"} except HTTPException: raise except Exception as e: logger.exception("Failed to get rooms for user %s: %s", user_id, e) raise e
async def find_room(query: Dict, config: Config) -> Dict: """get a room's ID using either the owner's ID or the room ID in the case of supplying the latter, this simply verifies that a room with that ID exists""" if "owner_id" in query: resource_name = "owner_id" query_method = rooms.RoomPersistence.get_room_by_owner elif "room_id" in query: resource_name = "room_id" query_method = rooms.RoomPersistence.get_room else: raise err.bad_request( "Supply either a room_id or an owner_id to search for") try: resource_id = int(query[resource_name]) except ValueError: raise err.bad_request( f"'{query[resource_name]}' is not a valid {resource_name}") try: async with config.get_database_connection() as db: room_persistence = rooms.RoomPersistence(db) room = await query_method(room_persistence, resource_id) if not room or not room.get("active"): raise err.not_found( f"No active rooms found for {resource_name} {resource_id}") else: return {"room_id": room["room_id"]} except HTTPException: raise except Exception as e: logger.exception("Failed to find room with query %s: %s", query, e) raise e
async def deactivate_room(user_id: int, room_id: int, config: Config) -> Dict: """Process a request from an owner to deactivate an owned room""" try: async with config.get_database_connection() as db: room_persistence = rooms.RoomPersistence(db) room = await room_persistence.get_room(room_id) if not room or not room.get("active"): raise err.not_found(f"Active room with id {room_id} not found") if room["owner_id"] != user_id: raise err.forbidden( "User is not permitted to deactivate this room") await room_persistence.deactivate_room(room_id) return { "success": True, "message": "Successfully deactivated the room" } except HTTPException: raise except Exception as e: logger.exception("Failed to deactivate rooms for user %s: %s", user_id, e) raise e
async def search(user_id: int, room_id: int, query: str, config: Config) -> Dict: if not query: raise err.bad_request("No query string supplied to search for") try: async with config.get_database_connection() as db: room = await get_room_for_user_assertive(room_id, user_id, db) token_result = await spotify.get_valid_token_for_user( room["owner_id"], config, requested_by=user_id) token = _handle_token_result(token_result) search_results = await config.get_spotify_api().search(query, token) return { "results": extract_relevant_data_from_search_results(search_results) } except HTTPException: raise except ClientResponseError as e: logger.warning( "Failed to enqueue track with Spotify for user %s in room %s: %s", user_id, room_id, e) if e.status == 403: raise err.forbidden( "Unable to enqueue track: user is not a premium subscriber") if e.status == 404: raise err.not_found("The requested track could not be found") raise e except Exception as e: logger.exception( "Failed to play track for user(id=%s) in room(id=%s): %s", user_id, room_id, e) raise
async def enqueue_song(user_id: int, room_id: int, track_uri: str, config: Config) -> Dict: try: async with config.get_database_connection() as db: room = await get_room_for_user_assertive(room_id, user_id, db) token_result = await spotify.get_valid_token_for_user( room["owner_id"], config, requested_by=user_id) token = _handle_token_result(token_result) await config.get_spotify_api().enqueue_song(track_uri, token) return {"success": True} except HTTPException: raise except ClientResponseError as e: logger.warning( "Failed to enqueue track with Spotify for user %s in room %s: %s", user_id, room_id, e) if e.status == 403: raise err.forbidden( "Unable to enqueue track: user is not a premium subscriber") if e.status == 404: raise err.not_found("The requested track could not be found") raise e except Exception as e: logger.exception( "Failed to play track for user(id=%s) in room(id=%s): %s", user_id, room_id, e) raise
async def spotify_auth(user_id: int, config: Config): """ Initiate authentication with the spotify API """ token = jwt.generate_jwt(user_id, jwt.Aud.API, config.jwt_key()) query_parameters = { "response_type": "code", "client_id": config.spotify_client_id(), "scope": REQUIRED_SCOPES, "redirect_uri": config.spotify_redirect(), "state": token } query_string = urlencode(query_parameters) redirect = f"https://accounts.spotify.com/authorize?{query_string}" logger.info("Redirecting user to %s", redirect) return {"url": redirect}
async def get_owned_room_for_user(user_id: int, config: Config) -> Dict: try: async with config.get_database_connection() as db: room_persistence = rooms.RoomPersistence(db) room = await room_persistence.get_room_by_owner(user_id) return room except Exception as e: logger.exception("Failed to create room for user %s: %s", user_id, e) raise e
async def get_room_by_id(room_id: int, user_id: int, config: Config) -> Dict: """ Delegates to the get_room_for_user_assertive method """ try: async with config.get_database_connection() as db: return await get_room_for_user_assertive(room_id, user_id, db) except Exception as e: logger.exception("Failed to retrieve room %s for user %s: %s", room_id, user_id, e) raise e
async def get_joined_rooms_for_user(user_id: int, config: Config) -> Dict: """Get rooms which the user is a member of""" try: async with config.get_database_connection() as db: room_persistence = rooms.RoomPersistence(db) joined_rooms = await room_persistence.get_joined_rooms_by_user( user_id) return {"rooms": joined_rooms} except Exception as e: logger.exception("Failed to get rooms for user %s: %s", user_id, e) raise e
async def get_valid_token_for_user( user_id: int, config: Config, requested_by=None) -> Union[str, GetTokenError]: try: async with config.get_database_connection() as db: token_persistence = spotify_token.SpotifyTokenPersistence(db) stored_token = await token_persistence.get_token_by_user(user_id) if not stored_token: # the user never auth'd return GetTokenError.NOT_AUTHED if not stored_token.get("access_token") and not stored_token.get( "refresh_token"): # we're not in a position to use or refresh the token and need the user to re-auth return GetTokenError.EXPIRED if not is_token_expired(stored_token): return stored_token["access_token"] elif not stored_token.get("refresh_token"): # user session has expired and is not refreshable if requested_by is not None and requested_by == user_id: # the owner is making the request, so ask them to re-auth await spotify_auth(user_id, config) return GetTokenError.EXPIRED # we have a refresh token to use response = await config.get_spotify_api().refresh_tokens( stored_token["refresh_token"]) created_at = datetime.utcnow() if not "access_token" in response: return GetTokenError.NOT_AUTHED await token_persistence.upsert_token( user_id, stored_token["spotify_user_id"], response["access_token"], response.get("refresh_token", stored_token["refresh_token"]), created_at, response["expires_in"]) return response["access_token"] except aiohttp.client_exceptions.ClientResponseError as e: logger.exception( "Something went wrong when refreshing tokens for user %s: %s", user_id, e) return GetTokenError.API_ERROR except DatabaseError as e: logger.exception( "Something went wrong retrieving or storing tokens for user %s: %s", user_id, e) return GetTokenError.DB_ERROR
async def handle_spotify_callback(query_params: Dict, config: Config): if query_params.get("error"): raise err.failed_dependency( "Please allow access to your Spotify account to proceed") token = query_params.get("state") if not token: raise err.bad_request( "'state' query parameter required but not provided") try: claims = jwt.get_claims_from_jwt(token, config.jwt_key(), jwt.Aud.API) user_id = int(claims["sub"]) except Exception as e: logger.debug("Failed to decode state as JWT: %s", e) raise err.bad_request("Invalid state from Spotify callback") code = query_params.get("code") if not code: raise err.failed_dependency( "Spotify authorization failed; no valid code returned") request = { "grant_type": "authorization_code", "code": code, "redirect_uri": config.spotify_redirect(), "client_id": config.spotify_client_id(), "client_secret": config.spotify_secret() } try: session = config.get_session() tokens_response = await config.get_spotify_api().request_tokens(request ) access_token = tokens_response["access_token"] refresh_token = tokens_response["refresh_token"] created_at = datetime.utcnow() expires_in = tokens_response["expires_in"] spotify_user_data = await config.get_spotify_api().spotify_user_data( access_token) spotify_user_id = spotify_user_data["id"] except Exception as e: logger.exception("Failed to authorize user(id=%s): %s", user_id, e) raise err.internal_server_error( "Something went wrong logging in with Spotify") try: async with config.get_database_connection() as db: await spotify_token.SpotifyTokenPersistence(db).upsert_token( user_id, spotify_user_id, access_token, refresh_token, created_at, expires_in) except Exception as e: logger.exception("Failed to upsert Spotify token for user(id=%s): %s", user_id, e) raise err.internal_server_error() return {"success": True}
async def create_room(user_id: int, room_code: Optional[str], room_name: str, config: Config) -> Dict: try: # only users that have auth'd with Spotify may create rooms token_result = await spotify.get_valid_token_for_user(user_id, config) token = _handle_token_result(token_result) async with config.get_database_connection() as db: room_persistence = rooms.RoomPersistence(db) room_id = await room_persistence.create_room( user_id, room_code, room_name) logger.debug("Created room(id=%s) for user(id=%s)", room_id, user_id) created_room = await room_persistence.get_room(room_id) return created_room except Exception as e: logger.exception("Failed to create room for user %s: %s", user_id, e) raise e
async def me(user_id: int, config: Config) -> Dict: try: async with config.get_database_connection() as db: get_user = users.UsersPersistence(db).get_user_by_id(user_id) get_joined_rooms = rooms.RoomPersistence( db).get_joined_rooms_by_user(user_id) get_token = spotify.get_valid_token_for_user(user_id, config) (user, joined_rooms, token) = await asyncio.gather(get_user, get_joined_rooms, get_token, return_exceptions=False) except Exception as e: logger.exception("Failed to get user(id=%s) from db: %s", user_id, e) raise err.internal_server_error() del user["password_hash"] user["rooms"] = joined_rooms user["authed_with_spotify"] = isinstance(token, str) return user