Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/components/AdminRegisterPage/AdminRegisterPage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ const mockEnterpriseCustomer = {
uuid: 'dc3bfcf8-c61f-11ec-9d64-0242ac120002',
};

const mockLoginRefreshResponse = {
data: {
access_token: 'mock-access-token',
refresh_token: 'mock-refresh-token',
},
};

const mockNavigate = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
Expand Down Expand Up @@ -66,6 +73,9 @@ describe('<AdminRegisterPage />', () => {
roles,
});
isEnterpriseUser.mockReturnValue(false);
LmsApiService.loginRefresh.mockImplementation(() => Promise.resolve({
data: mockLoginRefreshResponse,
}));
LmsApiService.fetchEnterpriseBySlug.mockImplementation(() => Promise.resolve({
data: mockEnterpriseCustomer,
}));
Expand All @@ -81,6 +91,9 @@ describe('<AdminRegisterPage />', () => {
roles: ['enterprise_admin:dc3bfcf8-c61f-11ec-9d64-0242ac120002'],
});
isEnterpriseUser.mockReturnValue(true);
LmsApiService.loginRefresh.mockImplementation(() => Promise.resolve({
data: mockLoginRefreshResponse,
}));
LmsApiService.fetchEnterpriseBySlug.mockImplementation(() => Promise.resolve({
data: mockEnterpriseCustomer,
}));
Expand Down
55 changes: 52 additions & 3 deletions src/components/AdminRegisterPage/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,59 @@ import { logError } from '@edx/frontend-platform/logging';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { LoginRedirect, getProxyLoginUrl } from '@edx/frontend-enterprise-logistration';
import { isEnterpriseUser, ENTERPRISE_ADMIN } from '@edx/frontend-enterprise-utils';
import { v5 as uuidv5 } from 'uuid';

import EnterpriseAppSkeleton from '../EnterpriseApp/EnterpriseAppSkeleton';
import LmsApiService from '../../data/services/LmsApiService';

/**
* AdminRegisterPage is a React component that manages the registration process for enterprise administrators.
*
* The component:
* - Redirects unauthenticated users to the enterprise proxy login flow.
* - For authenticated users:
* - Checks if they have the `enterprise_admin` JWT role associated with the specified enterprise.
* - Redirects users with the `enterprise_admin` role to the account activation page.
* - Redirects other authenticated users to the proxy login URL to refresh their JWT cookie
* and retrieve updated role assignments.
* - Fetches enterprise information by slug and processes the authenticated enterprise admin state.
* - Ensures that users visiting the register page for the first time will have the page reloaded
* for proper initialization.
*
* Dependencies:
* - `getAuthenticatedUser`: Retrieves the currently authenticated user.
* - `useParams`: React Router hook used to extract route parameters.
* - `useNavigate`: React Router hook for programmatically navigating to different routes.
* - `LmsApiService.fetchEnterpriseBySlug`: Fetches enterprise details by their slug.
* - `LmsApiService.loginRefresh`: Refreshes user login session and retrieves user details.
* - `isEnterpriseUser`: Validates if a user has a specific role within an enterprise.
* - `getProxyLoginUrl`: Constructs a URL for redirecting to the enterprise proxy login flow.
* - `uuidv5`: Used to generate a unique identifier for storage purposes.
* - `EnterpriseAppSkeleton`: Component displayed as a loading or placeholder state.
* - `LoginRedirect`: Component to handle login redirection with a loading display.
*
* Side Effects:
* - Utilizes `useEffect` to handle asynchronous data fetching, user authentication validation, and navigation.
* - Stores a flag in `localStorage` to identify if a user is visiting the register page for the first time.
* - Logs errors encountered during the asynchronous operations.
*
* Returns:
* - If the user is not authenticated, renders the `LoginRedirect` component for managing redirection
* to the proxy login flow.
* - If the user is authenticated, renders the `EnterpriseAppSkeleton` as a placeholder or loading
* state during processing.
*/
const AdminRegisterPage = () => {
const user = getAuthenticatedUser();
const { enterpriseSlug } = useParams();
const navigate = useNavigate();

useEffect(() => {
if (!user) {
return;
}
const processEnterpriseAdmin = (enterpriseUUID) => {
const isEnterpriseAdmin = isEnterpriseUser(user, ENTERPRISE_ADMIN, enterpriseUUID);
const authenticatedUser = getAuthenticatedUser();
const isEnterpriseAdmin = isEnterpriseUser(authenticatedUser, ENTERPRISE_ADMIN, enterpriseUUID);
if (isEnterpriseAdmin) {
// user is authenticated and has the ``enterprise_admin`` JWT role, so redirect user to
// account activation page to ensure they verify their email address.
Expand All @@ -40,7 +78,18 @@ const AdminRegisterPage = () => {
logError(error);
}
};
getEnterpriseBySlug();
// Force a fetch of a new JWT token and reload the page prior to any redirects.
// Importantly, only reload the page on first visit, or else risk infinite reloads.
LmsApiService.loginRefresh().then(data => {
const obfuscatedId = uuidv5(String(data.userId), uuidv5.DNS);
const storageKey = `first_visit_register_page_${obfuscatedId}`;
if (!localStorage.getItem(storageKey)) {
localStorage.setItem(storageKey, 'true');
window.location.reload();
}
return data;
}).catch(error => logError(error));
getEnterpriseBySlug().catch(error => logError(error));
Comment on lines +83 to +92
Copy link
Contributor

@pwnage101 pwnage101 Nov 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This is a relatively important piece of business logic and IMO needs a correspondingly importing looking code comment.

Suggested change
LmsApiService.loginRefresh().then(data => {
const obfuscatedId = uuidv5(String(data.userId), uuidv5.DNS);
const storageKey = `first_visit_register_page_${obfuscatedId}`;
if (!localStorage.getItem(storageKey)) {
localStorage.setItem(storageKey, 'true');
window.location.reload();
}
return data;
}).catch(error => logError(error));
getEnterpriseBySlug().catch(error => logError(error));
// Force a fetch of a new JWT token and reload the page prior to any redirects.
// Importantly, only reload the page on first visit, or else risk infinite reloads.
LmsApiService.loginRefresh().then(data => {
const obfuscatedId = uuidv5(String(data.userId), uuidv5.DNS);
const storageKey = `first_visit_register_page_${obfuscatedId}`;
if (!localStorage.getItem(storageKey)) {
localStorage.setItem(storageKey, 'true');
window.location.reload();
}
return data;
}).catch(error => logError(error));
getEnterpriseBySlug().catch(error => logError(error));

}, [user, navigate, enterpriseSlug]);

if (!user) {
Expand Down
1 change: 0 additions & 1 deletion src/components/UserActivationPage/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ const UserActivationPage = () => {
</>
);
}

// user data is hydrated with an unverified email address, so display a warning message since
// they have not yet verified their email via the "Activate your account" flow, so we should
// prevent access to the Admin Portal.
Expand Down
8 changes: 8 additions & 0 deletions src/data/services/LmsApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ class LmsApiService {

static enterpriseLearnerUrl = `${LmsApiService.baseUrl}/enterprise/api/v1/enterprise-learner/`;

static loginRefreshUrl = `${LmsApiService.baseUrl}/login_refresh`;

static async createEnterpriseGroup(
{
groupName,
Expand Down Expand Up @@ -638,6 +640,12 @@ class LmsApiService {
response.data = camelCaseObject(response.data);
return response;
};

static loginRefresh = async () => {
const url = LmsApiService.loginRefreshUrl;
const response = await LmsApiService.apiClient().post(url);
return camelCaseObject(response.data);
};
}

export default LmsApiService;