From 3785908c0b2cd3ae438a64e446035aded42d9101 Mon Sep 17 00:00:00 2001 From: Melissa Autumn Date: Thu, 23 May 2024 13:24:38 -0700 Subject: [PATCH 1/7] Hook up invite codes to the login screen --- backend/src/appointment/routes/auth.py | 16 ++++++++++++++ frontend/src/locales/en.json | 1 + frontend/src/views/LoginView.vue | 29 ++++++++++++++++++++------ 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/backend/src/appointment/routes/auth.py b/backend/src/appointment/routes/auth.py index ee599de23..4339a5cee 100644 --- a/backend/src/appointment/routes/auth.py +++ b/backend/src/appointment/routes/auth.py @@ -24,6 +24,7 @@ from ..controller import auth from ..controller.apis.fxa_client import FxaClient from ..dependencies.fxa import get_fxa_client +from ..exceptions import validation from ..exceptions.fxa_api import NotInAllowListException from ..l10n import l10n @@ -48,6 +49,7 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None): def fxa_login(request: Request, email: str, timezone: str | None = None, + invite_code: str | None = None, db: Session = Depends(get_db), fxa_client: FxaClient = Depends(get_fxa_client)): """Request an authorization url from fxa""" @@ -64,6 +66,7 @@ def fxa_login(request: Request, request.session['fxa_state'] = state request.session['fxa_user_email'] = email request.session['fxa_user_timezone'] = timezone + request.session['fxa_user_invite_code'] = invite_code return { 'url': url @@ -99,11 +102,14 @@ def fxa_callback( email = request.session['fxa_user_email'] # We only use timezone during subscriber creation, or if their timezone is None timezone = request.session['fxa_user_timezone'] + invite_code = request.session.get('fxa_user_invite_code') # Clear session keys request.session.pop('fxa_state') request.session.pop('fxa_user_email') request.session.pop('fxa_user_timezone') + if invite_code: + request.session.pop('fxa_user_invite_code') fxa_client.setup() @@ -123,11 +129,21 @@ def fxa_callback( new_subscriber_flow = not fxa_subscriber and not subscriber if new_subscriber_flow: + # Ensure the invite code exists and is available + # Use some inline-errors for now. We don't have a good error flow! + if not repo.invite.code_exists(db, invite_code): + raise HTTPException(404, l10n('invite-code-not-valid')) + if not repo.invite.code_is_available(db, invite_code): + raise HTTPException(403, l10n('invite-code-not-valid')) + subscriber = repo.subscriber.create(db, schemas.SubscriberBase( email=email, username=email, timezone=timezone, )) + + # Use the invite code after we've created the new subscriber + repo.invite.use_code(db, code, subscriber.id) elif not subscriber: subscriber = fxa_subscriber diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index e8c232b84..a8ebaf2dc 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -166,6 +166,7 @@ "guest": "{count} guests | {count} guest | {count} guests", "immediately": "instant", "inPerson": "In person", + "inviteCode": "Have an invite code?", "language": "Choose language", "legal": "Legal", "light": "Light", diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue index 0ae888741..7dd54c909 100644 --- a/frontend/src/views/LoginView.vue +++ b/frontend/src/views/LoginView.vue @@ -8,11 +8,10 @@ {{ loginError }}
-
@@ -41,9 +41,9 @@ {{ fieldData.value }} - {{ fieldData.value }} - {{ fieldData.value }} - {{ fieldData.value }} + {{ fieldData.value }} + {{ fieldData.value }} + {{ fieldData.value }} @@ -122,13 +122,13 @@ const updatePage = (index) => { const columnSpan = computed(() => (columns.value.length + (allowMultiSelect.value ? 1 : 0))); const selectedFields = ref([]); -const mutableDataList = ref([]); +const mutableDataList = ref(null); /** * Returns either a filtered data list, or the original all nice and paginated */ const paginatedDataList = computed(() => { - if (mutableDataList?.value?.length) { + if (mutableDataList?.value !== null) { return mutableDataList.value.slice(currentPage.value * pageSize, (currentPage.value + 1) * pageSize); } if (dataList?.value?.length) { @@ -138,6 +138,16 @@ const paginatedDataList = computed(() => { return []; }); +const totalDataLength = computed(() => { + if (mutableDataList?.value !== null) { + return mutableDataList.value?.length ?? 0; + } + if (dataList?.value?.length) { + return dataList.value.length; + } + return 0; +}); + const onFieldSelect = (evt, fieldData) => { const isChecked = evt?.target?.checked; @@ -154,8 +164,9 @@ const onFieldSelect = (evt, fieldData) => { const onColumnFilter = (evt, filter) => { mutableDataList.value = filter.fn(evt.target.value, dataList.value); + console.log('Data list info: ', mutableDataList.value, ' vs ', dataList.value); if (mutableDataList.value === dataList.value) { - mutableDataList.value = []; + mutableDataList.value = null; } }; diff --git a/frontend/src/elements/ListPagination.vue b/frontend/src/elements/ListPagination.vue index 313fad212..2fd7f1573 100644 --- a/frontend/src/elements/ListPagination.vue +++ b/frontend/src/elements/ListPagination.vue @@ -1,7 +1,7 @@ @@ -39,19 +39,13 @@ const props = defineProps({ listLength: Number, // number of total items in the displayed list pageSize: Number, // number of items per page }); -const emit = defineEmits(['update']) +const emit = defineEmits(['update']); const currentPage = ref(0); // index of active page -const isFirstPage = computed(() => { - return currentPage.value <= 0; -}); -const pageCount = computed(() => { - return Math.ceil(props.listLength/props.pageSize); -}); -const isLastPage = computed(() => { - return currentPage.value >= pageCount.value-1; -}); +const isFirstPage = computed(() => currentPage.value <= 0); +const pageCount = computed(() => Math.ceil(props.listLength / props.pageSize) || 1); +const isLastPage = computed(() => currentPage.value >= pageCount.value - 1); const prev = () => { if (!isFirstPage.value) { @@ -70,23 +64,15 @@ const goto = (index) => { emit('update', currentPage.value); }; -const showPageItem = (p) => { - return pageCount.value < 6 || p == 1 || p == 2 || isVisibleInnerPage(p) || p == pageCount.value-1; -}; -const showFirstEllipsis = (p) => { - return pageCount.value >= 6 && currentPage.value > 2 && p == 2; -}; -const showPageItemLink = (p) => { - return pageCount.value < 6 || p == 1 || isVisibleInnerPage(p); -}; -const showLastEllipsis = (p) => { - return pageCount.value >= 6 && currentPage.value < pageCount.value-3 && p == pageCount.value-1; -}; +const showPageItem = (p) => pageCount.value < 6 || p == 1 || p == 2 || isVisibleInnerPage(p) || p == pageCount.value - 1; +const showFirstEllipsis = (p) => pageCount.value >= 6 && currentPage.value > 2 && p == 2; +const showPageItemLink = (p) => pageCount.value < 6 || p == 1 || isVisibleInnerPage(p); +const showLastEllipsis = (p) => pageCount.value >= 6 && currentPage.value < pageCount.value - 3 && p == pageCount.value - 1; const isVisibleInnerPage = (p) => (currentPage.value == 0 && p == 3) || ((currentPage.value == 0 || currentPage.value == 1) && p == 4) - || (p > currentPage.value-1 && p < currentPage.value+3) - || ((currentPage.value == pageCount.value-1 || currentPage.value == pageCount.value-2) && p == pageCount.value-3) - || (currentPage.value == pageCount.value-1 && p == pageCount.value-2) + || (p > currentPage.value - 1 && p < currentPage.value + 3) + || ((currentPage.value == pageCount.value - 1 || currentPage.value == pageCount.value - 2) && p == pageCount.value - 3) + || (currentPage.value == pageCount.value - 1 && p == pageCount.value - 2) || p == pageCount.value; diff --git a/frontend/src/elements/admin/AdminNav.vue b/frontend/src/elements/admin/AdminNav.vue new file mode 100644 index 000000000..c3eaca730 --- /dev/null +++ b/frontend/src/elements/admin/AdminNav.vue @@ -0,0 +1,27 @@ + + + diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index a8ebaf2dc..daeaffce1 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -76,6 +76,8 @@ "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", diff --git a/frontend/src/router.js b/frontend/src/router.js index 713a367b4..7b7afd9fa 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -14,7 +14,8 @@ const AppointmentsView = defineAsyncComponent(() => import('@/views/Appointments const SettingsView = defineAsyncComponent(() => import('@/views/SettingsView')); const ProfileView = defineAsyncComponent(() => import('@/views/ProfileView')); const LegalView = defineAsyncComponent(() => import('@/views/LegalView')); -const SubscriberPanelView = defineAsyncComponent(() => import('@/views/SubscriberPanelView')); +const SubscriberPanelView = defineAsyncComponent(() => import('@/views/admin/SubscriberPanelView')); +const InviteCodePanelView = defineAsyncComponent(() => import('@/views/admin/InviteCodePanelView.vue')); /** * Defined routes for Thunderbird Appointment @@ -91,11 +92,17 @@ const routes = [ name: 'terms', component: LegalView, }, + // Admin { path: '/admin/subscribers', name: 'admin-subscriber-panel', component: SubscriberPanelView, }, + { + path: '/admin/invites', + name: 'admin-invite-codes-panel', + component: InviteCodePanelView, + }, ]; // create router object to export diff --git a/frontend/src/views/admin/InviteCodePanelView.vue b/frontend/src/views/admin/InviteCodePanelView.vue new file mode 100644 index 000000000..a222e4054 --- /dev/null +++ b/frontend/src/views/admin/InviteCodePanelView.vue @@ -0,0 +1,261 @@ + + + diff --git a/frontend/src/views/SubscriberPanelView.vue b/frontend/src/views/admin/SubscriberPanelView.vue similarity index 97% rename from frontend/src/views/SubscriberPanelView.vue rename to frontend/src/views/admin/SubscriberPanelView.vue index 2bb1e13fa..b59b9466b 100644 --- a/frontend/src/views/SubscriberPanelView.vue +++ b/frontend/src/views/admin/SubscriberPanelView.vue @@ -14,6 +14,9 @@ {{ pageError }} + + +