Zero state browsing with universal link#
Status#
DRAFT
Context#
Today, the only way to access our learner portal is by explicit, named invitation from an administrator sent from the admin portal. The invitation can only be sent while assigning a subsidy (code or license). Our cost-conscious admins need a way to publicize their catalog and gauge interest before assigning a subsidy (spend).
We want to allow admins to generate a universal link that can be shared with their enterprise. Users visiting the universal link would be able to browse the enterprise catalog and request course access.
We will take a two-phase approach for the implementation of universal links.
Phase 1#
Users visiting the link would be able to logistrate and be automatically associated with the enterprise. Users linked to an enterprise through a universal link will have no subsidies assigned and be in the “zero state”. Users in the “zero state” are shown an aggregate of all the catalogs for an enterprise on the search page and have access to all the tools that do not require a subsidy. They would then be able to browse the offerings and request course access.
Phase 2#
We want to make enterprise catalogs publicly accessible through a universal link so that users who don’t want to logistrate can still view a catalog. Users visiting this link would be able to browse the course offerings and request access. Upon course request, users will logistrate and be redirected to the learner portal course access request flow. Note that a user must be authenticated and associated with an enterprise to request course access.
Decision#
Phase 1 - Browsing via the learner portal search page#
Currently a PendingEnterpriseCustomerUser
is created whenever a subsidy is assigned that associates a user to an enterprise through email.
Upon logistration, we check for for any PendingEnterpriseCustomerUser
and links a user by replacing PendingEnterpriseCustomerUser
with
an EnterpriseCustomerUser
and assigning the enterprise_learner role.
A user has to be associated with an enterprise to have the correct permissions when accessing our APIs. We have to link a user to an enterprise in order to support browsing without a subsidy. This can be accomplished by exposing a secured endpoint that links a user to an enterprise which gets called by the learner portal. A user linked this way will be in the “zero state” by default and be able to browse the learner portal freely.
A new endpoint POST /link-enterprise/
that links a user and an enterprise will be created.
The payload will contain enterprise_customer_uuid
, user_id
, and enterprise_customer_key
- a UUID generated for the enterprise
A new model EnterpriseCustomerInviteKey
will be created to to stores these keys.
EnterpriseCustomerInviteKey
contains the following fields
* uuid - key used to generate the universal link/proof that a user had the universal link
* enterprise_customer_uuid - uuid of the enterprise
* usage_limit - the number of times a key can be used to link users
* expiry_date - defaults to the end date of the enterprise’s subscription plan
* timestamps
CRUD endpoints for creating/deleteing an EnterpriseCustomerInviteKey
model will also be created.
A universal link can be generated by adding the key as a query param to the url that points to the learner portal for an enterprise,
i.e. /?enterprise_customer_key=key
. An enterprise admin will be able to generate/view links through the admin portal
by calling POST /EnterpriseCustomerInviteKey/
to generate a new key. Note that only enterprise admins should be able to generate a key, and only
for enterprises they are an admin of. The generated link could also include the enterprise slug, but relying on the enterprise_customer_key
instead
to direct the user to the correct enterprise means that the link would still work if an enterprise changes its slug.
A user will be prompted to logistrate when visiting the link if not already logged in and be redirected to the learner portal afterwards.
Currently, a not found page is shown on the learner portal if the user is not linked. If the user is not linked and an enterprise_customer_key
is found in the query params,
the portal will call the new endpoint to link the user. If the call succeeds, the portal will reload and the user will be able to browse as usual.
If the call fails, appropriate messaging regarding the failure will be shown to the user.
This will also link users that were already logged in before visiting the link.
Limiting logistration through universal link#
Any authenticated user with a universal link that contains a valid key is able to link themselves with an enterprise. We want to take necessary precautions to prevent abuse of this feature.
EnterpriseCustomerInviteKey will be expirable and have a limit on the number of times it can be used. We will validate the key (not expired and usage limit has not been reached)
when linking a user to an enterprise. If the key is invalid, the user will not be linked and be informed that the link is invalid. We will also validate that
the enterprise_customer_uuid
in the request payload matches with the enterprise_customer_uuid
associated with the key.
Limiting number of course requests to prevent email spamming#
In the future we want to limit the number of course requests to prevent admins from being spammed with request notifications. This should be factored in when implementing course request and will not be addressed here.
Expiring a universal link#
A universal link can have a lifetime dictated by the expiry date set on the EnterpriseCustomerInviteKey
used in the link. A key will not be
valid if expired. An enterprise admin should be able to manually expire a key to revoke a link.
Analytics#
Segment events will be fired when
* an enterprise_customer_key
is created - i.e. ‘edx.bi.user.enterprise.key.created’
* a user is linked to an enterprise using the universal link, include key used in the event - ‘edx.bi.user.enterprise.key.used’
* a key expires or reaches its usage cap - ‘edx.bi.user.enterprise.key.invalidated’
* a user attempts to use a bad key - ‘edx.bi.user.enterprise.key.attempted’
We will also keep track of which user were linked using a universal link in the model (possibily a new field on EnterpriseCustomerUser
) and include that information
in future tracking events, i.e. search events in the learner portal.
We also want to track how many users land on the logistration page but abandons the page. This could possibly be done through google analytics.
Consequences#
After a user requests course access and is granted a subsidy(ies), the learner portal will only display offerings associated with the subsidy(ies).
The JWT token will need to be refreshed to include the
enterprise_learner
role after a user has been linked. The learner portal has logic to refresh the token if there are no roles included.The generated link would point to the learner portal search by default but having the
enterprise_customer_key
should allow a user to be linked from any page. We might want to redirect a user back to the search afterwards and strip the key from the url.Although key usage can be limited, users might still get unintended access to an
enterprise_customer_key
and link themselves to an enterprise. The admin portal will add a feature to list all of the learners associated with an enterprise and allow admins to manually remove them.
Phase 2 - Browsing anonymously#
To support anonymous browsing, we will create a public page on the learner portal. Anyone with a universal link will be able to browse an enterprise catalog and request course access on this page. The learner portal will make calls to new/modified endpoints to fetch data and display the catalog. The phase 1 implementation enables users to be associated with an enterprise which is required for requesting course access. Note that the anonymous browsing component could be built before the phase 1 implementation if it doesn’t the include course access request.
The process of generating the universal link will remain largely the same as in phase 1. The link would point to this new page rather than the search page,
ie. /catalog?enterprise_customer_key=key
. We will also validate the enterprise_customer_key
in the query params before rendering the page.
Revoking a link will also be the same process as in phase 1.
Since the learner portal makes calls to protected endpoints, we have to add/modify them to support the new public catalog page. The following are the APIs that the learner portal interacts with to display the enterprise catalog/course information:
edx-enterprise
GET /enterprise-customer/
Fetches enterprise customer data such as uuid, enterprise_customer_catalogs, etc. by the enterprise slug.
This is a protected endpoint.
There is a lite version
GET /enterprise-customer/basic_list
that currently returns only id and name of an enterprise. We will modify this to also include the enterprise_customer_catalogs and that will be the minimal information we need to render the page.
Algolia search API
Queries Algolia for catalog data.
This only requires an api key which the learner portal already has access to.
course-discovery
GET /courses/{key}/
Queries course-discovery to get course information once a user clicks on a course.
This is a protected endpoint.
Enterprise catalog also hosts course data. We could expose
enterprise-catalogs/get_content_metadata/{key}
as a public endpoint for the learner portal to query course data without going through course-discovery. The enterprise-catalog is synced daily with course-discovery.
enterprise-catalog
GET /contains_content_items/
Checks whether or not the specified content is available to the EnterpriseCustomer.
This is a protected endpoint.
We will make this endpoint public.
Authenticated users would not be able to view this page and instead be redirected to the normal search page since it’s outside of the normal learner portal flow.
Consequences#
We have to expose data through public endpoints which leads to concerns. For any endpoints we add/modify, we have to keep the amount of information returned to the minimum.
An
enterprise_customer_key
remains valid until it’s expired or the usage limit has been reached. However the usage limit only refers to the number of users that can be linked using the key. If we want to limit the number of times the key can be used to view the public catalog, we can keep track the number of unique visits and add a constraint.Having a public catalog could potentially increase the load on our system. This is not a huge concern and we will monitor the number of calls made.