From 7f012c67504ad44c38d12505356781862721495a Mon Sep 17 00:00:00 2001 From: Anuj Gupta <84966248+Anuj-Gupta4@users.noreply.github.com> Date: Fri, 20 Dec 2024 21:30:01 +0545 Subject: [PATCH] feat(backend): send org approval message to creator (#2008) * feat: approval notification for organisation approval in the background * feat(osm): enhance message sending by allowing recipient username or ID --- src/backend/app/auth/auth_deps.py | 1 - src/backend/app/auth/providers/osm.py | 50 +++++++++++++++++++ src/backend/app/helpers/helper_routes.py | 1 + .../app/organisations/organisation_crud.py | 37 +++++++++++++- .../app/organisations/organisation_routes.py | 19 ++++++- 5 files changed, 105 insertions(+), 3 deletions(-) diff --git a/src/backend/app/auth/auth_deps.py b/src/backend/app/auth/auth_deps.py index bc3276e9fd..4bea8abdea 100644 --- a/src/backend/app/auth/auth_deps.py +++ b/src/backend/app/auth/auth_deps.py @@ -273,7 +273,6 @@ async def login_required( request, settings.cookie_name, # FMTM cookie ) - print("manage") return await _authenticate_user(extracted_token) diff --git a/src/backend/app/auth/providers/osm.py b/src/backend/app/auth/providers/osm.py index 04268275ca..7d860a6ab7 100644 --- a/src/backend/app/auth/providers/osm.py +++ b/src/backend/app/auth/providers/osm.py @@ -21,7 +21,9 @@ import os from time import time +import requests from fastapi import Request, Response +from fastapi.exceptions import HTTPException from loguru import logger as log from osm_login_python.core import Auth @@ -107,3 +109,51 @@ async def handle_osm_callback(request: Request, osm_auth: Auth): ) return response_plus_cookies + + +def get_osm_token(request: Request, osm_auth: Auth) -> str: + """Extract and deserialize OSM token from cookies.""" + cookie_name = f"{settings.cookie_name}_osm" + log.debug(f"Extracting OSM token from cookie {cookie_name}") + serialised_osm_token = request.cookies.get(cookie_name) + if not serialised_osm_token: + raise HTTPException( + status_code=HTTPStatus.UNAUTHORIZED, + detail="You must be logged in to your OpenStreetMap account.", + ) + return osm_auth.deserialize_data(serialised_osm_token) + + +def send_osm_message( + osm_token: str, + title: str, + body: str, + osm_username: str = None, + osm_id: int = None, +) -> None: + """Send a message via OSM API.""" + if not osm_username and not osm_id: + raise ValueError("Either recipient or recipient_id must be provided.") + + email_url = f"{settings.OSM_URL}api/0.6/user/messages" + headers = {"Authorization": f"Bearer {osm_token}"} + post_body = { + "title": title, + "body": body, + } + + if osm_id: + post_body["recipient_id"] = osm_id + else: + post_body["recipient"] = osm_username + + log.debug( + f"Sending message to user ({osm_id or osm_username}) via OSM API: {email_url}" + ) + response = requests.post(email_url, headers=headers, data=post_body) + + if response.status_code == 200: + log.info("Message sent successfully") + else: + msg = "Sending message via OSM failed" + log.error(f"{msg}: {response.text}") diff --git a/src/backend/app/helpers/helper_routes.py b/src/backend/app/helpers/helper_routes.py index 5f37167e41..2d31574012 100644 --- a/src/backend/app/helpers/helper_routes.py +++ b/src/backend/app/helpers/helper_routes.py @@ -313,6 +313,7 @@ async def send_test_osm_message( > Notes section """) + # NOTE post body should contain either recipient or recipient_id post_body = { "recipient_id": 16289154, # "recipient_id": current_user.id, diff --git a/src/backend/app/organisations/organisation_crud.py b/src/backend/app/organisations/organisation_crud.py index 39608cd767..43c3a50ae6 100644 --- a/src/backend/app/organisations/organisation_crud.py +++ b/src/backend/app/organisations/organisation_crud.py @@ -17,11 +17,19 @@ # """Logic for organisation management.""" -from fastapi import UploadFile +from textwrap import dedent + +from fastapi import ( + Request, + UploadFile, +) +from loguru import logger as log +from osm_login_python.core import Auth from psycopg import Connection from psycopg.rows import class_row from app.auth.auth_schemas import AuthUser +from app.auth.providers.osm import get_osm_token, send_osm_message from app.config import settings from app.db.enums import MappingLevel, UserRole from app.db.models import DbOrganisation, DbOrganisationManagers, DbUser @@ -118,3 +126,30 @@ async def get_my_organisations( async with db.cursor(row_factory=class_row(OrganisationOut)) as cur: await cur.execute(sql, {"user_id": current_user.id}) return await cur.fetchall() + + +async def send_approval_message( + request: Request, + creator_id: int, + organisation_name: str, + osm_auth: Auth, +): + """Send message to the organisation creator after approval.""" + log.info(f"Sending approval message to organisation creator ({creator_id}).") + osm_token = get_osm_token(request, osm_auth) + message_content = dedent(f""" + ## Congratulations! + + Your organisation **{organisation_name}** has been approved. + + You can now manage your organisation freely. + + Thank you for being a part of our platform! + """) + send_osm_message( + osm_token=osm_token, + osm_id=creator_id, + title="Your organisation has been approved!", + body=message_content, + ) + log.info(f"Approval message sent to organisation creator ({creator_id}).") diff --git a/src/backend/app/organisations/organisation_routes.py b/src/backend/app/organisations/organisation_routes.py index ed9443c864..07896a3f53 100644 --- a/src/backend/app/organisations/organisation_routes.py +++ b/src/backend/app/organisations/organisation_routes.py @@ -21,9 +21,11 @@ from fastapi import ( APIRouter, + BackgroundTasks, Depends, File, HTTPException, + Request, Response, UploadFile, ) @@ -32,6 +34,7 @@ from app.auth.auth_deps import login_required from app.auth.auth_schemas import AuthUser, OrgUserDict +from app.auth.providers.osm import init_osm_auth from app.auth.roles import org_admin, super_admin from app.db.database import db_conn from app.db.enums import HTTPStatus @@ -128,13 +131,18 @@ async def delete_unapproved_org( @router.post("/approve", response_model=OrganisationOut) async def approve_organisation( + request: Request, org_id: int, + background_tasks: BackgroundTasks, db: Annotated[Connection, Depends(db_conn)], current_user: Annotated[AuthUser, Depends(super_admin)], + osm_auth=Depends(init_osm_auth), ): """Approve the organisation request made by the user. - The logged in user must be super admin to perform this action . + The logged in user must be super admin to perform this action. + + A background task notifies the organisation creator. """ log.info(f"Approving organisation ({org_id}).") approved_org = await DbOrganisation.update( @@ -148,6 +156,15 @@ async def approve_organisation( db, approved_org.id, approved_org.created_by ) + log.info(f"Approved organisation ({org_id}).") + background_tasks.add_task( + organisation_crud.send_approval_message, + request=request, + creator_id=approved_org.created_by, + organisation_name=approved_org.name, + osm_auth=osm_auth, + ) + return approved_org