diff options
| -rw-r--r-- | jb/managers/email_manager.py | 43 | ||||
| -rw-r--r-- | jb/managers/thl.py | 29 | ||||
| -rw-r--r-- | jb/views/auth.py | 44 | ||||
| -rw-r--r-- | jb/views/utils.py | 20 | ||||
| -rw-r--r-- | jb/views/wallet.py | 10 |
5 files changed, 90 insertions, 56 deletions
diff --git a/jb/managers/email_manager.py b/jb/managers/email_manager.py index 410e9a3..8006844 100644 --- a/jb/managers/email_manager.py +++ b/jb/managers/email_manager.py @@ -1,10 +1,14 @@ import requests from generalresearch.config import is_debug +from generalresearch.currency import USDCent +from generalresearch.models.thl.wallet.cashout_method import CashoutMethodOut from jb.config import settings MAUTIC_BASE_URL = "https://mail.jamesbillings67.com" -EMAIL_TEMPLATE_ID = 1 +LOGIN_EMAIL_TEMPLATE_ID = 1 +# Todo: We need a new template for the cashout confirmation +CASHOUT_EMAIL_TEMPLATE_ID = 3 assert settings.mautic_api_key auth_headers = {"Authorization": f"Basic {settings.mautic_api_key.get_secret_value()}"} @@ -26,11 +30,14 @@ def send_login_email_from_url(mautic_url: str, magic_link: str) -> None: if is_debug(): print("MAGIC_LINK: ", magic_link) return - email_tokens = { "magic_link": magic_link, } - body = {"tokens": email_tokens} + send_email_with_tokens(email_tokens, mautic_url) + + +def send_email_with_tokens(tokens: dict[str, str], mautic_url: str) -> None: + body = {"tokens": tokens} response = requests.post(url=mautic_url, json=body, headers=auth_headers) try: @@ -47,9 +54,7 @@ def send_login_email_from_url(mautic_url: str, magic_link: str) -> None: def send_login_email(email: str, magic_token: str) -> None: contact_id = get_or_create_contact(email=email) - mautic_url = ( - f"{MAUTIC_BASE_URL}/api/emails/{EMAIL_TEMPLATE_ID}/contact/{contact_id}/send" - ) + mautic_url = f"{MAUTIC_BASE_URL}/api/emails/{LOGIN_EMAIL_TEMPLATE_ID}/contact/{contact_id}/send" magic_link = f"{settings.base_url}auth/magic-link/?token={magic_token}" send_login_email_from_url(mautic_url, magic_link) @@ -58,17 +63,27 @@ def send_amt_link_email(email: str, magic_token: str) -> None: # Don't actually associate the email with the worker ID # until they click the link contact_id = get_or_create_contact(email=email) - mautic_url = ( - f"{MAUTIC_BASE_URL}/api/emails/{EMAIL_TEMPLATE_ID}/contact/{contact_id}/send" - ) + mautic_url = f"{MAUTIC_BASE_URL}/api/emails/{LOGIN_EMAIL_TEMPLATE_ID}/contact/{contact_id}/send" magic_link = f"{settings.base_url}auth/link-amt/?token={magic_token}" send_login_email_from_url(mautic_url, magic_link) -def send_cashout_confirmation_email(email: str, token: str) -> None: +def send_cashout_confirmation_email( + email: str, token: str, cashout_method: CashoutMethodOut, amount: USDCent +) -> None: contact_id = get_or_create_contact(email=email) - mautic_url = ( - f"{MAUTIC_BASE_URL}/api/emails/{EMAIL_TEMPLATE_ID}/contact/{contact_id}/send" - ) + mautic_url = f"{MAUTIC_BASE_URL}/api/emails/{CASHOUT_EMAIL_TEMPLATE_ID}/contact/{contact_id}/send" magic_link = f"{settings.base_url}wallet/cashout/confirm/?token={token}" - send_login_email_from_url(mautic_url, magic_link) + if is_debug(): + print("MAGIC_LINK: ", magic_link) + return + email_tokens = { + "magic_link": magic_link, + "cashout_method_name": cashout_method.name, + "cashout_method_description": cashout_method.description, + "cashout_method_type": cashout_method.type.value, + "amount": amount.to_usd_str(), + } + if cashout_method.image_url: + email_tokens["image_url"] = cashout_method.image_url + send_email_with_tokens(email_tokens, mautic_url) diff --git a/jb/managers/thl.py b/jb/managers/thl.py index 59bd2ab..4d6b23a 100644 --- a/jb/managers/thl.py +++ b/jb/managers/thl.py @@ -5,6 +5,7 @@ from generalresearch.models.thl.definitions import PayoutStatus from generalresearch.models.thl.payout import UserPayoutEvent from generalresearch.models.thl.task_status import TaskStatusResponse from generalresearch.models.thl.wallet.cashout_method import ( + CashoutMethodOut, CashoutRequestInfo, CashoutRequestResponse, ) @@ -63,17 +64,16 @@ def manage_pending_cashout( return UserPayoutEvent.model_validate(d) -def get_paypal_cashout_method_if_exists(product_user_id: str) -> str | None: +def get_paypal_cashout_method_if_exists( + product_user_id: str, client_ip: str +) -> str | None: """ - Todo: This does not work right now for new users, in that this endpoint expects the user - to have IP history or the request made from the client - (b/c it needs their country to know which tango card are available). - We don't care about that for paypal, but, it expects it anyways. - Once a user can change their paypal email, this needs to change to avoid overwriting it - every time they login. + Make sure a paypal cashout method exists for this user. Use their login email. + We check if it exists first, so that once a user can change their paypal email, + we do not overwrite it. """ url = f"{settings.fsb_host}{settings.product_id}/cashout_methods/" - params = {"bpuid": product_user_id} + params = {"bpuid": product_user_id, "ip": client_ip} res = requests.get(url, params=params) assert res.status_code == status.HTTP_200_OK, res.text cms = res.json()["cashout_methods"] @@ -92,12 +92,21 @@ def create_paypal_cashout_method(product_user_id: str, email: str) -> dict: def create_paypal_cashout_method_if_not_exists( - product_user_id: str, email: str + product_user_id: str, email: str, client_ip: str ) -> None: - if not get_paypal_cashout_method_if_exists(product_user_id): + if not get_paypal_cashout_method_if_exists(product_user_id, client_ip=client_ip): create_paypal_cashout_method(product_user_id, email) +def get_cashout_method(cashout_method_id: str) -> CashoutMethodOut: + url = ( + f"{settings.fsb_host}{settings.product_id}/cashout_methods/{cashout_method_id}/" + ) + res = requests.get(url) + assert res.status_code == status.HTTP_200_OK, res.text + return CashoutMethodOut.model_validate(res.json()["cashout_method"]) + + def get_wallet_balance(amt_worker_id: str) -> USDCent: # This will raise an Exception if wallet balance is negative url = f"{settings.fsb_host}{settings.product_id}/wallet/" diff --git a/jb/views/auth.py b/jb/views/auth.py index 36b013f..aa3cf03 100644 --- a/jb/views/auth.py +++ b/jb/views/auth.py @@ -2,7 +2,7 @@ import secrets from typing import Annotated from urllib.parse import urlencode -from fastapi import APIRouter, Depends, Header, HTTPException, Response, status +from fastapi import APIRouter, Depends, Header, HTTPException, Request, Response, status from fastapi.responses import HTMLResponse, RedirectResponse from jb.api.auth import ( @@ -26,10 +26,7 @@ from jb.managers.email_manager import ( send_login_email, ) from jb.managers.gr_api import GRApiManager -from jb.managers.thl import ( - create_paypal_cashout_method_if_not_exists, - create_paypal_cashout_method, -) +from jb.managers.thl import create_paypal_cashout_method_if_not_exists from jb.models.auth import ( AccountLogin, AmtAccountLink, @@ -37,6 +34,7 @@ from jb.models.auth import ( User, ) from jb.settings import render_base_html +from jb.views.utils import get_client_ip auth_router = APIRouter(prefix="/auth", tags=["Auth"]) @@ -80,6 +78,7 @@ def request_magic_link(body: AccountLogin) -> dict[str, str]: @auth_router.get("/magic-link/", response_class=HTMLResponse, include_in_schema=False) def magic_link_landing_page( + request: Request, gr_api: Annotated[GRApiManager, Depends(get_gr_api_manager)], token: str | None = None, ) -> Response: @@ -91,7 +90,8 @@ def magic_link_landing_page( detail="token is required", ) response = RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER) - _exchange_magic_link(token, response, gr_api) + client_ip = get_client_ip(request) + _exchange_magic_link(token, response, gr_api, client_ip=client_ip) return response return HTMLResponse( render_base_html(), @@ -105,23 +105,27 @@ def magic_link_landing_page( @auth_router.post("/magic-link/exchange/", status_code=status.HTTP_204_NO_CONTENT) def exchange_magic_link( + request: Request, body: MagicLinkExchangeRequest, response: Response, gr_api: Annotated[GRApiManager, Depends(get_gr_api_manager)], ) -> None: """Exchange a magic link only after its landing page makes an explicit POST.""" - _exchange_magic_link(body.token, response, gr_api) + client_ip = get_client_ip(request) + _exchange_magic_link(body.token, response, gr_api, client_ip=client_ip) -def _exchange_magic_link(token: str, response: Response, gr_api: GRApiManager) -> None: +def _exchange_magic_link( + token: str, response: Response, gr_api: GRApiManager, client_ip: str +) -> None: user_email = consume_magic_token(token) user = gr_api.ensure_user_exists(User.model_validate({"email": user_email})) # create_paypal_cashout_method_if_not_exists( # product_user_id=user.product_user_id, email=user.email # ) - create_paypal_cashout_method( - product_user_id=user.product_user_id, email=user.email + create_paypal_cashout_method_if_not_exists( + product_user_id=user.product_user_id, email=user.email, client_ip=client_ip ) response.set_cookie( @@ -173,6 +177,7 @@ def invite_amt_account_link( @auth_router.get("/debug/", response_class=HTMLResponse, include_in_schema=False) def link_amt_account_landing_page( + request: Request, gr_api: Annotated[GRApiManager, Depends(get_gr_api_manager)], token: str | None = None, ) -> HTMLResponse: @@ -187,9 +192,11 @@ def link_amt_account_landing_page( status_code=status.HTTP_400_BAD_REQUEST, detail="token is required", ) - + client_ip = get_client_ip(request) _response = RedirectResponse(url="/", status_code=status.HTTP_303_SEE_OTHER) - _exchange_amt_account_link(token=token, response=_response, gr_api=gr_api) + _exchange_amt_account_link( + token=token, response=_response, gr_api=gr_api, client_ip=client_ip + ) return HTMLResponse( render_base_html(), @@ -203,27 +210,32 @@ def link_amt_account_landing_page( @auth_router.post("/link-amt/exchange/", status_code=status.HTTP_204_NO_CONTENT) def exchange_amt_account_link( + request: Request, body: MagicLinkExchangeRequest, response: Response, gr_api: Annotated[GRApiManager, Depends(get_gr_api_manager)], ) -> None: """Validate the email link, then transition the bound AMT account.""" + client_ip = get_client_ip(request) try: - _exchange_amt_account_link(body.token, response, gr_api) + _exchange_amt_account_link(body.token, response, gr_api, client_ip=client_ip) except ValueError as e: LOG.error(f"Failed to exchange AMT account link: {e}") raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) -def _exchange_amt_account_link(token: str, response: Response, gr_api: GRApiManager): +def _exchange_amt_account_link( + token: str, response: Response, gr_api: GRApiManager, client_ip: str +): token_data = consume_amt_account_link_token(token) email = token_data.email amt_worker_id = token_data.amt_worker_id user = User(email=email) user = gr_api.transition_user_from_amt(user=user, amt_worker_id=amt_worker_id) - # create_paypal_cashout_method_if_not_exists(product_user_id=user.product_user_id, email=user.email) - create_paypal_cashout_method(product_user_id=user.product_user_id, email=user.email) + create_paypal_cashout_method_if_not_exists( + product_user_id=user.product_user_id, email=user.email, client_ip=client_ip + ) # In Mautic, associate the email with the worker ID (AFTER the user has transitioned) get_or_create_contact(email=email, amt_worker_id=amt_worker_id) diff --git a/jb/views/utils.py b/jb/views/utils.py index 0d08e9b..a133263 100644 --- a/jb/views/utils.py +++ b/jb/views/utils.py @@ -2,17 +2,11 @@ from fastapi import Request def get_client_ip(request: Request) -> str: - """ - Using a testclient, the ip returned is 'testclient'. If so, instead, grab - the ip from the headers - """ - ip = request.headers.get("X-Forwarded-For") - if not ip: - ip = request.client.host # type: ignore - elif ip == "testclient" or ip.startswith("10."): - forwarded = request.headers.get("X-Forwarded-For") - ip = ( - forwarded.split(",")[0].strip() if forwarded else request.client.host # type: ignore - ) + forwarded = request.headers.get("X-Forwarded-For") + if forwarded: + return forwarded.split(",", 1)[0].strip() - return ip + if request.client is None: + raise ValueError("Client IP is unavailable") + + return request.client.host diff --git a/jb/views/wallet.py b/jb/views/wallet.py index c281698..9fda533 100644 --- a/jb/views/wallet.py +++ b/jb/views/wallet.py @@ -9,7 +9,7 @@ from jb.api.auth import get_authenticated_user from jb.api.cashout_token import consume_cashout_token, create_cashout_token from jb.config import settings from jb.managers.email_manager import send_cashout_confirmation_email -from jb.managers.thl import user_cashout_request +from jb.managers.thl import get_cashout_method, user_cashout_request from jb.models.auth import User from jb.models.wallet import CashoutConfirmation, CashoutRequest, PendingCashout from jb.settings import render_base_html @@ -33,10 +33,14 @@ def request_cashout( query = urlencode({"token": token}) confirmation_link = f"{settings.base_url}wallet/cashout/confirm/?{query}" + cm = get_cashout_method(cashout_method_id=body.cashout_method_id) + if settings.debug: - return {"confirmation_link": confirmation_link} + return {"confirmation_link": confirmation_link, "cashout_method": cm.id} - send_cashout_confirmation_email(email=str(user.email), token=token) + send_cashout_confirmation_email( + email=str(user.email), token=token, cashout_method=cm, amount=body.amount + ) return {"detail": "Confirmation sent. Check your inbox to finish the cashout."} |
