From 0a0c9bac35c1515a7f3ece8fa7c11356a5006a88 Mon Sep 17 00:00:00 2001 From: Melissa Autumn Date: Thu, 23 May 2024 14:59:18 -0700 Subject: [PATCH] Add invite generation, and fix using invites during user registration --- backend/src/appointment/routes/auth.py | 7 +- backend/src/appointment/routes/invite.py | 15 ----- backend/test/conftest.py | 1 + backend/test/integration/test_auth.py | 7 +- frontend/src/locales/en.json | 7 +- .../src/views/admin/InviteCodePanelView.vue | 65 +++++++++++-------- 6 files changed, 54 insertions(+), 48 deletions(-) diff --git a/backend/src/appointment/routes/auth.py b/backend/src/appointment/routes/auth.py index 4339a5cee..e690e454d 100644 --- a/backend/src/appointment/routes/auth.py +++ b/backend/src/appointment/routes/auth.py @@ -143,7 +143,12 @@ def fxa_callback( )) # Use the invite code after we've created the new subscriber - repo.invite.use_code(db, code, subscriber.id) + used = repo.invite.use_code(db, invite_code, subscriber.id) + + # This shouldn't happen, but just in case! + if not used: + repo.subscriber.delete(db, subscriber) + raise HTTPException(500, l10n('unknown-error')) elif not subscriber: subscriber = fxa_subscriber diff --git a/backend/src/appointment/routes/invite.py b/backend/src/appointment/routes/invite.py index 5a6864bda..fce7d20ee 100644 --- a/backend/src/appointment/routes/invite.py +++ b/backend/src/appointment/routes/invite.py @@ -30,21 +30,6 @@ def generate_invite_codes(n: int, db: Session = Depends(get_db), _admin: Subscri return repo.invite.generate_codes(db, n) -@router.put("/redeem/{code}") -def use_invite_code(code: str, db: Session = Depends(get_db)): - raise NotImplementedError - - """endpoint to create a new subscriber and update the corresponding invite""" - if not repo.invite.code_exists(db, code): - raise validation.InviteCodeNotFoundException() - if not repo.invite.code_is_available(db, code): - raise validation.InviteCodeNotAvailableException() - # TODO: get email from admin panel - email = 'placeholder@mozilla.org' - subscriber = repo.subscriber.create(db, schemas.SubscriberBase(email=email, username=email)) - return repo.invite.use_code(db, code, subscriber.id) - - @router.put("/revoke/{code}") def revoke_invite_code(code: str, db: Session = Depends(get_db), admin: Subscriber = Depends(get_admin_subscriber)): """endpoint to revoke a given invite code and mark in unavailable, needs admin permissions""" diff --git a/backend/test/conftest.py b/backend/test/conftest.py index 78e63d45f..03906fd38 100644 --- a/backend/test/conftest.py +++ b/backend/test/conftest.py @@ -20,6 +20,7 @@ from factory.schedule_factory import make_schedule # noqa: F401 from factory.slot_factory import make_appointment_slot # noqa: F401 from factory.subscriber_factory import make_subscriber, make_basic_subscriber, make_pro_subscriber # noqa: F401 +from factory.invite_factory import make_invite # Load our env load_dotenv(find_dotenv(".env.test")) diff --git a/backend/test/integration/test_auth.py b/backend/test/integration/test_auth.py index 5efbf3905..c4aecb57c 100644 --- a/backend/test/integration/test_auth.py +++ b/backend/test/integration/test_auth.py @@ -59,16 +59,19 @@ def test_fxa_login(self, with_client): assert 'url' in data assert data.get('url') == FXA_CLIENT_PATCH.get('authorization_url') - def test_fxa_callback(self, with_db, with_client, monkeypatch): + def test_fxa_callback(self, with_db, with_client, monkeypatch, make_invite): """Test that our callback function correctly handles the session states, and creates a new subscriber""" os.environ['AUTH_SCHEME'] = 'fxa' state = 'a1234' + invite = make_invite() + monkeypatch.setattr('starlette.requests.HTTPConnection.session', { 'fxa_state': state, 'fxa_user_email': FXA_CLIENT_PATCH.get('subscriber_email'), - 'fxa_user_timezone': 'America/Vancouver' + 'fxa_user_timezone': 'America/Vancouver', + 'fxa_user_invite_code': invite.code, }) response = with_client.get( diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index daeaffce1..07b4d63e4 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -68,6 +68,7 @@ "bookingSuccessfullyRequested": "Booking request sent", "copiedToClipboard": "Copied to clipboard", "eventWasCreated": "Event created in your calendar.", + "invitationGenerated": "The invite codes were generated successfully.", "invitationWasSent": "An invitation was sent to your email.", "invitationWasSentContext": "An invitation was sent to {email}.", "messageWasNotSent": "Your message couldn't be sent. Please try again.", @@ -76,12 +77,12 @@ "slotIsAvailableAgain": "Time slot now available for bookings." }, "label": { - "admin-invite-codes-panel": "Invites", - "admin-subscriber-panel": "Subscribers", "12hAmPm": "12h AM/PM", "24h": "24h", "DDMMYYYY": "DD/MM/YYYY", "MMDDYYYY": "MM/DD/YYYY", + "admin-invite-codes-panel": "Invites", + "admin-subscriber-panel": "Subscribers", "account": "Account", "accountData": "Download your account data", "accountDeletion": "Delete your account", @@ -94,6 +95,7 @@ "allAppointments": "All bookings", "allDay": "All day", "allFutureAppointments": "All future bookings", + "amountOfCodes": "Amount of codes to generate", "appearance": "Appearance", "appointmentCreationError": "Oh no! Something went wrong.", "appointmentDetails": "Details", @@ -162,6 +164,7 @@ "filter": "Filter", "general": "General", "generalDetails": "Details", + "generate": "Generate", "generateZoomLink": "Generate Zoom Meeting", "goBack": "Go Back", "google": "Google", diff --git a/frontend/src/views/admin/InviteCodePanelView.vue b/frontend/src/views/admin/InviteCodePanelView.vue index a222e4054..0bcdc44b7 100644 --- a/frontend/src/views/admin/InviteCodePanelView.vue +++ b/frontend/src/views/admin/InviteCodePanelView.vue @@ -30,20 +30,19 @@ @@ -77,7 +76,7 @@ const dj = inject('dayjs'); const invites = ref([]); const displayPage = ref(false); -const inviteEmail = ref(''); +const generateCodeAmount = ref(null); const loading = ref(true); const pageError = ref(''); const pageNotification = ref(''); @@ -164,19 +163,31 @@ const filters = [ * @returns {*} */ fn: (selectedKey, mutableDataList) => { - if (selectedKey === 'all') { - return mutableDataList; - } - if (selectedKey === 'revoked') { - return mutableDataList.filter((data) => (data.status.value === 'Revoked')); - } - return mutableDataList.filter((data) => { - if (data.status.value === 'Revoked') { - return false; - } + switch (selectedKey) { + case 'all': + return null; + case 'revoked': + return mutableDataList.filter((data) => (data.status.value === 'Revoked')); + case 'used': + return mutableDataList.filter((data) => { + if (data.status.value === 'Revoked') { + return false; + } - return selectedKey === 'used' && data.subscriber_id.value !== 'Unused'; - }); + return data.subscriber_id.value !== 'Unused'; + }); + case 'unused': + return mutableDataList.filter((data) => { + if (data.status.value === 'Revoked') { + return false; + } + + return data.subscriber_id.value === 'Unused'; + }); + default: + break; + } + return null; }, }, ]; @@ -211,14 +222,12 @@ const revokeInvite = async (code) => { } }; -const sendInvite = async () => { +const generateInvites = async () => { loading.value = true; pageError.value = ''; pageNotification.value = ''; - const response = await call('invite/send').post({ - email: inviteEmail.value, - }).json(); + const response = await call(`invite/generate/${generateCodeAmount.value}`).post().json(); const { data, error } = response; @@ -233,8 +242,8 @@ const sendInvite = async () => { pageError.value = data.value?.detail?.message; } } else { - pageNotification.value = t('info.invitationWasSentContext', { email: inviteEmail.value }); - inviteEmail.value = ''; + pageNotification.value = t('info.invitationGenerated'); + generateCodeAmount.value = null; await refresh(); }