-
Notifications
You must be signed in to change notification settings - Fork 229
feat(admin): expose accountAuthorizations on the admin panel #20534
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,8 @@ | |
| import { expect, test } from '../../lib/fixtures/standard'; | ||
|
|
||
| const ADMIN_PANEL_URL = process.env.ADMIN_PANEL_URL ?? 'http://localhost:8091'; | ||
| const ADMIN_SERVER_URL = process.env.ADMIN_SERVER_URL ?? 'http://localhost:8095'; | ||
| const ADMIN_SERVER_URL = | ||
| process.env.ADMIN_SERVER_URL ?? 'http://localhost:8095'; | ||
|
|
||
| // Admin panel tests only run locally (stage/prod require SSO) | ||
| test.skip(({ target }) => target.name !== 'local'); | ||
|
|
@@ -67,4 +68,48 @@ test.describe('Admin Panel', () => { | |
| timeout: 10000, | ||
| }); | ||
| }); | ||
|
|
||
| test('account by-uid response includes accountAuthorizations field', async ({ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we wanting to expand current admin-panel functional tests? I figured we had the ones we have now as a sanity check. I'm OK with leaving but I feel like admin-panel could also mostly just be unit tests. |
||
| target, | ||
| testAccountTracker, | ||
| }) => { | ||
| const details = testAccountTracker.generateAccountDetails(); | ||
| const credentials = await target.createAccount( | ||
| details.email, | ||
| details.password | ||
| ); | ||
|
|
||
| const res = await fetch( | ||
| `${ADMIN_SERVER_URL}/api/account/by-uid?uid=${encodeURIComponent(credentials.uid)}`, | ||
| { | ||
| headers: { | ||
| 'oidc-claim-id-token-email': 'test-admin@mozilla.com', | ||
| 'remote-groups': 'vpn_fxa_admin_panel_prod', | ||
| }, | ||
| } | ||
| ); | ||
| expect(res.status).toBe(200); | ||
| const data = await res.json(); | ||
| expect(Array.isArray(data.accountAuthorizations)).toBe(true); | ||
| // A brand-new account has no browser-service authorizations on record. | ||
| expect(data.accountAuthorizations).toEqual([]); | ||
| }); | ||
|
|
||
| test('admin panel renders the authorized browser services section', async ({ | ||
| target, | ||
| page, | ||
| testAccountTracker, | ||
| }) => { | ||
| const credentials = testAccountTracker.generateAccountDetails(); | ||
| await target.createAccount(credentials.email, credentials.password); | ||
|
|
||
| await page.goto(`${ADMIN_PANEL_URL}/account-search`); | ||
| await page.getByTestId('email-input').fill(credentials.email); | ||
| await page.getByTestId('search-button').click(); | ||
|
|
||
| await expect( | ||
| page.getByRole('heading', { name: /authorized browser services/i }) | ||
| ).toBeVisible({ timeout: 10000 }); | ||
| await expect(page.getByTestId('account-authorizations-none')).toBeVisible(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,7 @@ import { AdminPanelFeature } from '@fxa/shared/guards'; | |
| import Guard from '../../Guard'; | ||
| import Subscription from '../Subscription'; | ||
| import { ConnectedServices } from '../ConnectedServices'; | ||
| import { AccountAuthorizations } from '../AccountAuthorizations'; | ||
| import { TableRowYHeader, TableYHeaders } from '../../TableYHeaders'; | ||
| import { TableRowXHeader, TableXHeaders } from '../../TableXHeaders'; | ||
| import EmailBounces from '../EmailBounces'; | ||
|
|
@@ -94,6 +95,7 @@ export const Account = ({ | |
| backupCodes, | ||
| recoveryPhone, | ||
| passkeys, | ||
| accountAuthorizations, | ||
| }: AccountProps) => { | ||
| const createdAtDate = getFormattedDate(createdAt); | ||
| const disabledAtDate = getFormattedDate(disabledAt); | ||
|
|
@@ -417,6 +419,9 @@ export const Account = ({ | |
| <Guard features={[AdminPanelFeature.ConnectedServices]}> | ||
| <h3 className="header-lg">Connected Services</h3> | ||
| <ConnectedServices services={attachedClients} /> | ||
|
|
||
| <h3 className="header-lg">Authorized Browser Services</h3> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since signing into RPs also updates this table, I feel like we could use one sentence explaining what this is? E.g. a Relay authorization doesn't necessarily mean the user is using Relay in the browser. We should also have a note here for support agents about "Sync" authorizations, related to the first bullet point here under caveats. |
||
| <AccountAuthorizations authorizations={accountAuthorizations} /> | ||
| </Guard> | ||
|
|
||
| <h3 className="header-lg">Account History</h3> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
|
||
| import { render, screen } from '@testing-library/react'; | ||
| import { AccountAuthorization } from 'fxa-admin-server/src/types'; | ||
| import { AccountAuthorizations } from '.'; | ||
|
|
||
| const AUTHORIZATIONS: AccountAuthorization[] = [ | ||
| { | ||
| service: 'sync', | ||
| scope: 'https://identity.mozilla.com/apps/oldsync', | ||
| authorizedAt: new Date('2026-01-01T00:00:00Z').getTime(), | ||
| }, | ||
| { | ||
| service: 'relay', | ||
| scope: 'https://identity.mozilla.com/apps/relay', | ||
| authorizedAt: new Date('2026-02-01T00:00:00Z').getTime(), | ||
| }, | ||
| ]; | ||
|
|
||
| it('renders the empty state when there are no authorizations', () => { | ||
| render(<AccountAuthorizations authorizations={[]} />); | ||
| expect(screen.getByTestId('account-authorizations-none')).toHaveTextContent( | ||
| 'This account has not authorized any browser services.' | ||
| ); | ||
| }); | ||
|
|
||
| it('renders the empty state when authorizations is null', () => { | ||
| render(<AccountAuthorizations authorizations={null} />); | ||
| expect(screen.getByTestId('account-authorizations-none')).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it('renders one row per authorization', () => { | ||
| render(<AccountAuthorizations authorizations={AUTHORIZATIONS} />); | ||
| const services = screen.getAllByTestId('account-authorization-service'); | ||
| const scopes = screen.getAllByTestId('account-authorization-scope'); | ||
| const dates = screen.getAllByTestId('account-authorization-authorized-at'); | ||
|
|
||
| expect(services).toHaveLength(2); | ||
| expect(scopes).toHaveLength(2); | ||
| expect(dates).toHaveLength(2); | ||
|
|
||
| expect(services[0]).toHaveTextContent('sync'); | ||
| expect(scopes[0]).toHaveTextContent( | ||
| 'https://identity.mozilla.com/apps/oldsync' | ||
| ); | ||
| expect(services[1]).toHaveTextContent('relay'); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| /* This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
|
||
| import { AccountAuthorization } from 'fxa-admin-server/src/types'; | ||
| import { TableRowXHeader, TableXHeaders } from '../../TableXHeaders'; | ||
| import { getFormattedDate } from '../../../lib/utils'; | ||
|
|
||
| export const AccountAuthorizations = ({ | ||
| authorizations, | ||
| }: { | ||
| authorizations?: Nullable<AccountAuthorization[]>; | ||
| }) => { | ||
| if (!authorizations || authorizations.length === 0) { | ||
| return ( | ||
| <p data-testid="account-authorizations-none" className="result-none"> | ||
| This account has not authorized any browser services. | ||
| </p> | ||
| ); | ||
| } | ||
|
|
||
| return ( | ||
| <TableXHeaders rowHeaders={['Service', 'Scope', 'Authorized At']}> | ||
| {authorizations.map(({ service, scope, authorizedAt }) => ( | ||
| <TableRowXHeader key={`${service}-${scope}`}> | ||
| <td data-testid="account-authorization-service">{service}</td> | ||
| <td data-testid="account-authorization-scope">{scope}</td> | ||
| <td data-testid="account-authorization-authorized-at"> | ||
| {getFormattedDate(authorizedAt)} | ||
| </td> | ||
| </TableRowXHeader> | ||
| ))} | ||
| </TableXHeaders> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is our one VPN test being flaky or just noticeably slow? Reading this comment, I'm not sure when we'd use this vs not, given we have a lot of webchannel tests without it 🤔