From c8c6bc9d4fe781d29f9f243c2eabb2b1cecac34f Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Mon, 22 Sep 2025 08:49:13 +1000 Subject: [PATCH 1/4] Add SBP registration functionality and related schemas - Introduced SBP registration request schema. - Implemented SBP user registration endpoint with email notification. - Updated platform enums and mappings to include SBP. - Added tests for SBP registration, including validation and error handling. --- db/types.py | 1 + main.py | 2 + routers/admin.py | 1 + routers/sbp_register.py | 109 +++++++++++++++++++++ schemas/biocommons.py | 24 ++++- schemas/sbp.py | 12 +++ tests/datagen.py | 8 ++ tests/test_sbp_register.py | 192 +++++++++++++++++++++++++++++++++++++ 8 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 routers/sbp_register.py create mode 100644 schemas/sbp.py create mode 100644 tests/test_sbp_register.py diff --git a/db/types.py b/db/types.py index 927a0975..0a7735a6 100644 --- a/db/types.py +++ b/db/types.py @@ -19,6 +19,7 @@ class ApprovalStatusEnum(str, Enum): class PlatformEnum(str, Enum): GALAXY = "galaxy" BPA_DATA_PORTAL = "bpa_data_portal" + SBP = "sbp" class PlatformMembershipData(BaseModel): diff --git a/main.py b/main.py index 064fcb46..ff7f90e5 100644 --- a/main.py +++ b/main.py @@ -14,6 +14,7 @@ biocommons_register, bpa_register, galaxy_register, + sbp_register, user, utils, ) @@ -61,5 +62,6 @@ def public_route(): app.include_router(biocommons_register.router) app.include_router(bpa_register.router) app.include_router(galaxy_register.router) +app.include_router(sbp_register.router) app.include_router(utils.router) app.include_router(biocommons_groups.router) diff --git a/routers/admin.py b/routers/admin.py index 67a21477..173439af 100644 --- a/routers/admin.py +++ b/routers/admin.py @@ -35,6 +35,7 @@ PLATFORM_MAPPING = { "galaxy": {"enum": PlatformEnum.GALAXY, "name": "Galaxy Australia"}, "bpa_data_portal": {"enum": PlatformEnum.BPA_DATA_PORTAL, "name": "Bioplatforms Australia Data Portal"}, + "sbp": {"enum": PlatformEnum.SBP, "name": "Structural Biology Platform"}, } GROUP_MAPPING = { diff --git a/routers/sbp_register.py b/routers/sbp_register.py new file mode 100644 index 00000000..2e88c608 --- /dev/null +++ b/routers/sbp_register.py @@ -0,0 +1,109 @@ +import logging + +from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException +from httpx import HTTPStatusError +from sqlmodel import Session +from starlette.responses import JSONResponse + +from auth.ses import EmailService +from auth0.client import Auth0Client, get_auth0_client +from config import Settings, get_settings +from db.models import BiocommonsUser, PlatformEnum +from db.setup import get_db_session +from routers.errors import RegistrationRoute +from schemas.biocommons import Auth0UserData, BiocommonsRegisterData +from schemas.responses import RegistrationErrorResponse, RegistrationResponse +from schemas.sbp import SBPRegistrationRequest + +logger = logging.getLogger(__name__) + +router = APIRouter( + prefix="/sbp", + tags=["sbp", "registration"], + # Overriding route class to handle registration errors + route_class=RegistrationRoute +) + + +def send_approval_email(registration: SBPRegistrationRequest): + """Send email notification about new SBP registration.""" + email_service = EmailService() + approver_email = "aai-dev@biocommons.org.au" + subject = "New SBP User Registration" + + body_html = f""" +

A new user has registered for the Structural Biology Platform.

+

User: {registration.first_name} {registration.last_name} ({registration.email})

+

Username: {registration.username}

+

Registration Reason: {registration.reason}

+

Please review and approve this registration in the admin panel.

+ """ + + email_service.send_email( + to_email=approver_email, + subject=subject, + body_html=body_html + ) + + +@router.post( + "/register", + responses={ + 200: {"model": RegistrationResponse}, + 400: {"model": RegistrationErrorResponse}, + }, +) +async def register_sbp_user( + registration: SBPRegistrationRequest, + background_tasks: BackgroundTasks, + db_session: Session = Depends(get_db_session), + auth0_client: Auth0Client = Depends(get_auth0_client), + settings: Settings = Depends(get_settings) +): + """Register a new SBP user.""" + + # Create Auth0 user data + user_data = BiocommonsRegisterData.from_sbp_registration( + registration=registration + ) + + try: + logger.info("Registering user with Auth0") + auth0_user_data = auth0_client.create_user(user_data) + + logger.info("Adding user to DB") + _create_sbp_user_record(auth0_user_data, db_session) + + # Send approval email in the background + if settings.send_email: + background_tasks.add_task(send_approval_email, registration) + logger.info("Approval email queued for sending") + + return {"message": "User registered successfully. Approval pending.", "user": auth0_user_data.model_dump(mode="json")} + + # Return HTTP status errors as RegistrationErrorResponse + except HTTPStatusError as e: + # Catch specific errors where possible and return a useful error message + if e.response.status_code == 409: + response = RegistrationErrorResponse(message="Username or email already in use") + else: + response = RegistrationErrorResponse(message=f"Auth0 error: {str(e.response.text)}") + return JSONResponse(status_code=400, content=response.model_dump(mode="json")) + # Unknown errors should return 500 + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Failed to register user: {str(e)}" + ) + + +def _create_sbp_user_record(auth0_user_data: Auth0UserData, session: Session) -> BiocommonsUser: + db_user = BiocommonsUser.from_auth0_data(data=auth0_user_data) + sbp_membership = db_user.add_platform_membership( + platform=PlatformEnum.SBP, + db_session=session, + auto_approve=False + ) + session.add(db_user) + session.add(sbp_membership) + session.commit() + return db_user diff --git a/schemas/biocommons.py b/schemas/biocommons.py index e74812c4..e232593f 100644 --- a/schemas/biocommons.py +++ b/schemas/biocommons.py @@ -68,7 +68,7 @@ def _check(v: str) -> str: return Annotated[str, AfterValidator(_check)] -AppId = Literal["biocommons", "galaxy", "bpa"] +AppId = Literal["biocommons", "galaxy", "bpa", "sbp"] BiocommonsUsername = ValidatedString(min_length=3, max_length=128, pattern="^[-_a-z0-9]+$", messages={ "min_length": "Username must be at least 3 characters.", "max_length": "Username must be 128 characters or less.", @@ -85,6 +85,10 @@ class BPAMetadata(BaseModel): registration_reason: str +class SBPMetadata(BaseModel): + registration_reason: str + + class BiocommonsUserMetadata(BaseModel): """ User metadata we use for user-changeable data @@ -92,6 +96,7 @@ class BiocommonsUserMetadata(BaseModel): """ bpa: Optional[BPAMetadata] = None + sbp: Optional[SBPMetadata] = None class BiocommonsAppMetadata(BaseModel): @@ -205,6 +210,23 @@ def from_bpa_registration( ), ) + @classmethod + def from_sbp_registration( + cls, registration: "schemas.sbp.SBPRegistrationRequest" + ) -> Self: + return cls( + email=registration.email, + password=registration.password, + username=registration.username, + name=f"{registration.first_name} {registration.last_name}", + user_metadata=BiocommonsUserMetadata( + sbp=SBPMetadata(registration_reason=registration.reason), + ), + app_metadata=BiocommonsAppMetadata( + registration_from="sbp" + ), + ) + @classmethod def from_galaxy_registration( cls, diff --git a/schemas/sbp.py b/schemas/sbp.py new file mode 100644 index 00000000..2a77e3ef --- /dev/null +++ b/schemas/sbp.py @@ -0,0 +1,12 @@ +from pydantic import BaseModel, EmailStr + +from schemas.biocommons import BiocommonsPassword, BiocommonsUsername + + +class SBPRegistrationRequest(BaseModel): + first_name: str + last_name: str + username: BiocommonsUsername + email: EmailStr + reason: str + password: BiocommonsPassword diff --git a/tests/datagen.py b/tests/datagen.py index 6441aaab..666c3a66 100644 --- a/tests/datagen.py +++ b/tests/datagen.py @@ -18,6 +18,7 @@ from schemas.biocommons_register import BiocommonsRegistrationRequest from schemas.bpa import BPARegistrationRequest from schemas.galaxy import GalaxyRegistrationData +from schemas.sbp import SBPRegistrationRequest from schemas.tokens import AccessTokenPayload from schemas.user import SessionUser @@ -113,6 +114,13 @@ class BPARegistrationDataFactory(ModelFactory[BPARegistrationRequest]): username = BiocommonsProviders.biocommons_username +class SBPRegistrationDataFactory(ModelFactory[SBPRegistrationRequest]): + """Factory for generating SBP registration test data.""" + + password = BiocommonsProviders.biocommons_password + username = BiocommonsProviders.biocommons_username + + class AppMetadataFactory(ModelFactory[BiocommonsAppMetadata]): ... diff --git a/tests/test_sbp_register.py b/tests/test_sbp_register.py new file mode 100644 index 00000000..f65a49c9 --- /dev/null +++ b/tests/test_sbp_register.py @@ -0,0 +1,192 @@ + +import httpx +import pytest +from sqlmodel import select + +from db.models import ( + BiocommonsUser, + PlatformEnum, + PlatformMembership, + PlatformMembershipHistory, +) +from schemas.biocommons import BiocommonsRegisterData +from tests.datagen import ( + Auth0UserDataFactory, + SBPRegistrationDataFactory, + random_auth0_id, +) + + +@pytest.fixture +def valid_registration_data(): + """Fixture that provides valid SBP registration data.""" + return SBPRegistrationDataFactory.build( + username="testuser", + first_name="Test", + last_name="User", + email="test@example.com", + reason="Need access to SBP resources", + password="SecurePass123!", + ).model_dump() + + +def test_to_biocommons_register_data(valid_registration_data): + sbp_data = SBPRegistrationDataFactory.build() + register_data = BiocommonsRegisterData.from_sbp_registration(sbp_data) + assert register_data.username == sbp_data.username + assert register_data.name == f"{sbp_data.first_name} {sbp_data.last_name}" + assert register_data.app_metadata.registration_from == "sbp" + + +def test_successful_registration( + test_client, valid_registration_data, mock_auth0_client, test_db_session +): + """Test successful user registration with SBP service""" + user_id = random_auth0_id() + mock_auth0_client.create_user.return_value = Auth0UserDataFactory.build( + user_id=user_id, + email=valid_registration_data["email"], + username=valid_registration_data["username"] + ) + + response = test_client.post("/sbp/register", json=valid_registration_data) + + assert response.status_code == 200 + data = response.json() + assert "message" in data + assert "Approval pending" in data["message"] + assert "user" in data + + # Check that user exists in database + user = test_db_session.exec(select(BiocommonsUser).where(BiocommonsUser.id == user_id)).first() + assert user is not None + assert user.email == valid_registration_data["email"] + assert user.username == valid_registration_data["username"] + + # Check that SBP membership is created but not approved + membership = test_db_session.exec( + select(PlatformMembership).where( + PlatformMembership.user_id == user_id, + PlatformMembership.platform_id == PlatformEnum.SBP + ) + ).first() + assert membership is not None + assert membership.approval_status == "pending" # Should be pending, not approved + + # Check that membership history is created + history = test_db_session.exec( + select(PlatformMembershipHistory).where( + PlatformMembershipHistory.user_id == user_id, + PlatformMembershipHistory.platform_id == PlatformEnum.SBP + ) + ).first() + assert history is not None + assert history.approval_status == "pending" + + +def test_registration_duplicate_user( + test_client, valid_registration_data, mock_auth0_client +): + """Test registration with duplicate username/email""" + # Mock Auth0 to return 409 Conflict + error_response = httpx.Response(409, text="User already exists") + mock_auth0_client.create_user.side_effect = httpx.HTTPStatusError( + "User exists", request=None, response=error_response + ) + + response = test_client.post("/sbp/register", json=valid_registration_data) + assert response.status_code == 400 + data = response.json() + assert "Username or email already in use" in data["message"] + + +def test_registration_auth0_error( + test_client, mock_auth0_client, valid_registration_data +): + """Test registration with Auth0 server error""" + error_response = httpx.Response(500, text="Internal server error") + mock_auth0_client.create_user.side_effect = httpx.HTTPStatusError( + "Server error", request=None, response=error_response + ) + + response = test_client.post("/sbp/register", json=valid_registration_data) + assert response.status_code == 400 + data = response.json() + assert "Auth0 error" in data["message"] + + +def test_registration_request_validation(test_client): + """Test registration with invalid data""" + invalid_data = { + "username": "", # Invalid: too short + "first_name": "", # Invalid: empty + "last_name": "", # Invalid: empty + "email": "invalid-email", # Invalid: not an email + "reason": "", # Invalid: empty + "password": "weak", # Invalid: doesn't meet requirements + } + + response = test_client.post("/sbp/register", json=invalid_data) + assert response.status_code == 400 # Validation error handled by RegistrationRoute + + +def test_registration_email_format(test_client, valid_registration_data): + """Test registration with invalid email format""" + valid_registration_data["email"] = "not-an-email" + + response = test_client.post("/sbp/register", json=valid_registration_data) + assert response.status_code == 400 + + +def test_registration_password_validation(test_client, valid_registration_data): + """Test registration with weak password""" + valid_registration_data["password"] = "weak" + + response = test_client.post("/sbp/register", json=valid_registration_data) + assert response.status_code == 400 + + +def test_registration_username_validation(test_client, valid_registration_data): + """Test registration with invalid username""" + valid_registration_data["username"] = "invalid username!" # Contains invalid characters + + response = test_client.post("/sbp/register", json=valid_registration_data) + assert response.status_code == 400 + + +def test_successful_registration_with_email_enabled( + test_client_with_email, valid_registration_data, mock_auth0_client, test_db_session, mocker +): + """Test successful registration when email sending is enabled""" + user_id = random_auth0_id() + mock_auth0_client.create_user.return_value = Auth0UserDataFactory.build(user_id=user_id) + + # Mock email service + mock_email_service = mocker.patch('routers.sbp_register.EmailService') + + response = test_client_with_email.post("/sbp/register", json=valid_registration_data) + + assert response.status_code == 200 + data = response.json() + assert "Approval pending" in data["message"] + + # Verify email service was called (it's called as a background task) + # Note: In a real test, you might need to wait for background tasks to complete + assert mock_email_service.called + + +def test_sbp_metadata_stored_correctly( + test_client, valid_registration_data, mock_auth0_client, test_db_session +): + """Test that SBP metadata is stored correctly in Auth0 user data""" + user_id = random_auth0_id() + mock_auth0_client.create_user.return_value = Auth0UserDataFactory.build(user_id=user_id) + + response = test_client.post("/sbp/register", json=valid_registration_data) + + assert response.status_code == 200 + + # Verify the user_data passed to Auth0 contains SBP metadata + call_args = mock_auth0_client.create_user.call_args[0][0] + assert call_args.user_metadata.sbp.registration_reason == valid_registration_data["reason"] + assert call_args.app_metadata.registration_from == "sbp" From 85d3e60902126d4227f31a05a345df29969210ec Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Mon, 22 Sep 2025 08:56:06 +1000 Subject: [PATCH 2/4] Update filter options test to include 'sbp' in expected results --- tests/test_admin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_admin.py b/tests/test_admin.py index 53fbb46f..84b087c8 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -336,7 +336,7 @@ def test_get_filter_options(test_client, as_admin_user): options = resp.json() assert isinstance(options, list) - assert len(options) == 4 + assert len(options) == 5 for option in options: assert "id" in option @@ -345,12 +345,13 @@ def test_get_filter_options(test_client, as_admin_user): assert isinstance(option["name"], str) option_ids = {opt["id"] for opt in options} - expected_ids = {"galaxy", "bpa_data_portal", "tsi", "bpa_galaxy"} + expected_ids = {"galaxy", "bpa_data_portal", "sbp", "tsi", "bpa_galaxy"} assert option_ids == expected_ids option_dict = {opt["id"]: opt["name"] for opt in options} assert option_dict["galaxy"] == "Galaxy Australia" assert option_dict["bpa_data_portal"] == "Bioplatforms Australia Data Portal" + assert option_dict["sbp"] == "Structural Biology Platform" assert option_dict["tsi"] == "Threatened Species Initiative Bundle" assert option_dict["bpa_galaxy"] == "Bioplatforms Australia Data Portal & Galaxy Australia Bundle" From e1efd3d8f3122fbf31d0abae4dab9f32890f8172 Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Mon, 22 Sep 2025 09:18:06 +1000 Subject: [PATCH 3/4] udpate tests --- tests/test_sbp_register.py | 122 +++++++++++-------------------------- 1 file changed, 34 insertions(+), 88 deletions(-) diff --git a/tests/test_sbp_register.py b/tests/test_sbp_register.py index f65a49c9..7256fe37 100644 --- a/tests/test_sbp_register.py +++ b/tests/test_sbp_register.py @@ -19,7 +19,6 @@ @pytest.fixture def valid_registration_data(): - """Fixture that provides valid SBP registration data.""" return SBPRegistrationDataFactory.build( username="testuser", first_name="Test", @@ -30,7 +29,7 @@ def valid_registration_data(): ).model_dump() -def test_to_biocommons_register_data(valid_registration_data): +def test_to_biocommons_register_data(): sbp_data = SBPRegistrationDataFactory.build() register_data = BiocommonsRegisterData.from_sbp_registration(sbp_data) assert register_data.username == sbp_data.username @@ -41,7 +40,6 @@ def test_to_biocommons_register_data(valid_registration_data): def test_successful_registration( test_client, valid_registration_data, mock_auth0_client, test_db_session ): - """Test successful user registration with SBP service""" user_id = random_auth0_id() mock_auth0_client.create_user.return_value = Auth0UserDataFactory.build( user_id=user_id, @@ -52,18 +50,13 @@ def test_successful_registration( response = test_client.post("/sbp/register", json=valid_registration_data) assert response.status_code == 200 - data = response.json() - assert "message" in data - assert "Approval pending" in data["message"] - assert "user" in data + assert "Approval pending" in response.json()["message"] - # Check that user exists in database - user = test_db_session.exec(select(BiocommonsUser).where(BiocommonsUser.id == user_id)).first() + user = test_db_session.get(BiocommonsUser, user_id) assert user is not None assert user.email == valid_registration_data["email"] assert user.username == valid_registration_data["username"] - # Check that SBP membership is created but not approved membership = test_db_session.exec( select(PlatformMembership).where( PlatformMembership.user_id == user_id, @@ -71,9 +64,8 @@ def test_successful_registration( ) ).first() assert membership is not None - assert membership.approval_status == "pending" # Should be pending, not approved + assert membership.approval_status == "pending" - # Check that membership history is created history = test_db_session.exec( select(PlatformMembershipHistory).where( PlatformMembershipHistory.user_id == user_id, @@ -83,110 +75,64 @@ def test_successful_registration( assert history is not None assert history.approval_status == "pending" + called_data = mock_auth0_client.create_user.call_args[0][0] + assert called_data.user_metadata.sbp.registration_reason == valid_registration_data["reason"] + assert called_data.app_metadata.registration_from == "sbp" + def test_registration_duplicate_user( test_client, valid_registration_data, mock_auth0_client ): - """Test registration with duplicate username/email""" - # Mock Auth0 to return 409 Conflict - error_response = httpx.Response(409, text="User already exists") - mock_auth0_client.create_user.side_effect = httpx.HTTPStatusError( - "User exists", request=None, response=error_response + error = httpx.HTTPStatusError( + "User already exists", + request=httpx.Request("POST", "https://api.example.com/data"), + response=httpx.Response(409, text="User already exists"), ) + mock_auth0_client.create_user.side_effect = error response = test_client.post("/sbp/register", json=valid_registration_data) + assert response.status_code == 400 - data = response.json() - assert "Username or email already in use" in data["message"] + assert response.json()["message"] == "Username or email already in use" def test_registration_auth0_error( test_client, mock_auth0_client, valid_registration_data ): - """Test registration with Auth0 server error""" - error_response = httpx.Response(500, text="Internal server error") - mock_auth0_client.create_user.side_effect = httpx.HTTPStatusError( - "Server error", request=None, response=error_response + error = httpx.HTTPStatusError( + "Server error", + request=httpx.Request("POST", "https://api.example.com/data"), + response=httpx.Response(400, text="Something went wrong"), ) + mock_auth0_client.create_user.side_effect = error response = test_client.post("/sbp/register", json=valid_registration_data) + assert response.status_code == 400 - data = response.json() - assert "Auth0 error" in data["message"] + assert response.json()["message"] == "Auth0 error: Something went wrong" def test_registration_request_validation(test_client): - """Test registration with invalid data""" invalid_data = { - "username": "", # Invalid: too short - "first_name": "", # Invalid: empty - "last_name": "", # Invalid: empty - "email": "invalid-email", # Invalid: not an email - "reason": "", # Invalid: empty - "password": "weak", # Invalid: doesn't meet requirements + "username": "testuser", + "email": "invalid-email", } response = test_client.post("/sbp/register", json=invalid_data) - assert response.status_code == 400 # Validation error handled by RegistrationRoute - -def test_registration_email_format(test_client, valid_registration_data): - """Test registration with invalid email format""" - valid_registration_data["email"] = "not-an-email" - - response = test_client.post("/sbp/register", json=valid_registration_data) assert response.status_code == 400 + error_data = response.json() + assert error_data["message"] == "Invalid data submitted" + assert any(error["field"] == "email" for error in error_data["field_errors"]) -def test_registration_password_validation(test_client, valid_registration_data): - """Test registration with weak password""" - valid_registration_data["password"] = "weak" - - response = test_client.post("/sbp/register", json=valid_registration_data) - assert response.status_code == 400 - +def test_registration_email_format(test_client, valid_registration_data): + data = valid_registration_data.copy() + data["email"] = "invalid-email" -def test_registration_username_validation(test_client, valid_registration_data): - """Test registration with invalid username""" - valid_registration_data["username"] = "invalid username!" # Contains invalid characters + response = test_client.post("/sbp/register", json=data) - response = test_client.post("/sbp/register", json=valid_registration_data) assert response.status_code == 400 - - -def test_successful_registration_with_email_enabled( - test_client_with_email, valid_registration_data, mock_auth0_client, test_db_session, mocker -): - """Test successful registration when email sending is enabled""" - user_id = random_auth0_id() - mock_auth0_client.create_user.return_value = Auth0UserDataFactory.build(user_id=user_id) - - # Mock email service - mock_email_service = mocker.patch('routers.sbp_register.EmailService') - - response = test_client_with_email.post("/sbp/register", json=valid_registration_data) - - assert response.status_code == 200 - data = response.json() - assert "Approval pending" in data["message"] - - # Verify email service was called (it's called as a background task) - # Note: In a real test, you might need to wait for background tasks to complete - assert mock_email_service.called - - -def test_sbp_metadata_stored_correctly( - test_client, valid_registration_data, mock_auth0_client, test_db_session -): - """Test that SBP metadata is stored correctly in Auth0 user data""" - user_id = random_auth0_id() - mock_auth0_client.create_user.return_value = Auth0UserDataFactory.build(user_id=user_id) - - response = test_client.post("/sbp/register", json=valid_registration_data) - - assert response.status_code == 200 - - # Verify the user_data passed to Auth0 contains SBP metadata - call_args = mock_auth0_client.create_user.call_args[0][0] - assert call_args.user_metadata.sbp.registration_reason == valid_registration_data["reason"] - assert call_args.app_metadata.registration_from == "sbp" + details = response.json() + errors = details["field_errors"] + assert "email" in [error["field"] for error in errors] From d56f4fd93f0a8f3c8f99484102a871d1f86742d9 Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Tue, 23 Sep 2025 10:56:23 +1000 Subject: [PATCH 4/4] Add AAI Portal URL to settings and update email templates for group and SBP registration approvals --- .env.example | 2 ++ config.py | 2 ++ routers/biocommons_groups.py | 6 +++--- routers/biocommons_register.py | 6 +++--- routers/sbp_register.py | 8 ++++---- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index 34fa12ea..95fd4e94 100644 --- a/.env.example +++ b/.env.example @@ -15,6 +15,8 @@ JWT_SECRET_KEY=secret-key ADMIN_ROLES='["Admin", "GalaxyAdmin"]' # Enable sending emails (for approving group memberships etc.) SEND_EMAIL=False +# AAI Portal URL for admin links in emails +AAI_PORTAL_URL=https://aaiportal.example.com # URL of Galaxy instance, for making calls to Galaxy API GALAXY_URL=https://galaxy.example.com GALAXY_API_KEY=api-key diff --git a/config.py b/config.py index bc3d9f76..1f1b93e8 100644 --- a/config.py +++ b/config.py @@ -19,6 +19,8 @@ class Settings(BaseSettings): # Note we process this separately in app startup as it needs # to be available before the app starts cors_allowed_origins: str + # AAI Portal URL for admin links in emails + aai_portal_url: str = "https://aaiportal.test.biocommons.org.au" model_config = SettingsConfigDict(env_file=".env", extra="ignore") diff --git a/routers/biocommons_groups.py b/routers/biocommons_groups.py index c6d36d97..541aad99 100644 --- a/routers/biocommons_groups.py +++ b/routers/biocommons_groups.py @@ -107,7 +107,7 @@ def request_group_access( admin_emails = membership.group.get_admins(auth0_client=auth0_client) for email in admin_emails: background_tasks.add_task(send_group_approval_email, - approver_email=email, request=membership, email_service=email_service) + approver_email=email, request=membership, email_service=email_service, settings=settings) return {"message": f"Group membership for {group_id} requested successfully."} @@ -148,13 +148,13 @@ def approve_group_access( return {"message": f"Group membership for {group.name} approved successfully."} -def send_group_approval_email(approver_email: str, request: GroupMembership, email_service: EmailService): +def send_group_approval_email(approver_email: str, request: GroupMembership, email_service: EmailService, settings: Settings): subject = f"New request to join {request.group.name}" body_html = f"""

A new user has requested access to the {request.group.name} group.

User: {request.user.email}

-

Please log into the BioCommons account dashboard to review and approve access.

+

Please log into the BioCommons account dashboard to review and approve access.

""" email_service.send(approver_email, subject, body_html) diff --git a/routers/biocommons_register.py b/routers/biocommons_register.py index 2a819bdd..6d86a93b 100644 --- a/routers/biocommons_register.py +++ b/routers/biocommons_register.py @@ -36,7 +36,7 @@ router = APIRouter(prefix="/biocommons", tags=["biocommons", "registration"], route_class=RegistrationRoute) -def send_approval_email(registration: BiocommonsRegistrationRequest): +def send_approval_email(registration: BiocommonsRegistrationRequest, settings: Settings): """Send email notification about new biocommons registration.""" email_service = EmailService() approver_email = "aai-dev@biocommons.org.au" @@ -48,7 +48,7 @@ def send_approval_email(registration: BiocommonsRegistrationRequest):

Username: {registration.username}

Selected Bundle: {registration.bundle}

Requested Access: BPA Data Portal & Galaxy Australia

-

Please log into the AAI Admin Portal to review and approve access.

+

Please log into the AAI Admin Portal to review and approve access.

""" email_service.send(approver_email, subject, body_html) @@ -82,7 +82,7 @@ async def register_biocommons_user( # Send approval email in background if settings.send_email: - background_tasks.add_task(send_approval_email, registration) + background_tasks.add_task(send_approval_email, registration, settings) logger.info( f"Successfully registered biocommons user: {auth0_user_data.user_id}" diff --git a/routers/sbp_register.py b/routers/sbp_register.py index 2e88c608..47641bbf 100644 --- a/routers/sbp_register.py +++ b/routers/sbp_register.py @@ -25,18 +25,18 @@ ) -def send_approval_email(registration: SBPRegistrationRequest): +def send_approval_email(registration: SBPRegistrationRequest, settings: Settings): """Send email notification about new SBP registration.""" email_service = EmailService() approver_email = "aai-dev@biocommons.org.au" - subject = "New SBP User Registration" + subject = "New Structural Biology Platform User Registration" body_html = f"""

A new user has registered for the Structural Biology Platform.

User: {registration.first_name} {registration.last_name} ({registration.email})

Username: {registration.username}

Registration Reason: {registration.reason}

-

Please review and approve this registration in the admin panel.

+

Please log into the AAI Admin Portal to review and approve access.

""" email_service.send_email( @@ -76,7 +76,7 @@ async def register_sbp_user( # Send approval email in the background if settings.send_email: - background_tasks.add_task(send_approval_email, registration) + background_tasks.add_task(send_approval_email, registration, settings) logger.info("Approval email queued for sending") return {"message": "User registered successfully. Approval pending.", "user": auth0_user_data.model_dump(mode="json")}