Skip to content

Commit

Permalink
frontend: Add support for naming user auth tokens (#64509)
Browse files Browse the repository at this point in the history
Add support for naming tokens when creating them
  • Loading branch information
mdtro authored Feb 14, 2024
1 parent 9d3ca66 commit cc97408
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 23 deletions.
1 change: 1 addition & 0 deletions fixtures/js-stubs/apiToken.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export function ApiTokenFixture(
): NewInternalAppApiToken {
return {
id: '1',
name: 'token_name1',
token: 'apitoken123',
dateCreated: new Date('Thu Jan 11 2018 18:01:41 GMT-0800 (PST)').toISOString(),
scopes: ['project:read', 'project:write'],
Expand Down
1 change: 1 addition & 0 deletions fixtures/js-stubs/sentryAppToken.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export function SentryAppTokenFixture(
): NewInternalAppApiToken {
return {
token: '123456123456123456123456-token',
name: 'apptokenname-1',
dateCreated: '2019-03-02T18:30:26Z',
scopes: [],
refreshToken: '123456123456123456123456-refreshtoken',
Expand Down
1 change: 1 addition & 0 deletions static/app/types/user.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ interface BaseApiToken {
dateCreated: string;
expiresAt: string;
id: string;
name: string;
scopes: Scope[];
state: string;
}
Expand Down
78 changes: 78 additions & 0 deletions static/app/views/settings/account/apiNewToken.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,82 @@ describe('ApiNewToken', function () {
)
);
});

it('creates token with optional name', async function () {
MockApiClient.clearMockResponses();
const assignMock = MockApiClient.addMockResponse({
method: 'POST',
url: `/api-tokens/`,
});

render(<ApiNewToken />, {
context: RouterContextFixture(),
});
const createButton = screen.getByRole('button', {name: 'Create Token'});

const selectByValue = (name, value) =>
selectEvent.select(screen.getByRole('textbox', {name}), value);

await selectByValue('Project', 'Admin');
await selectByValue('Release', 'Admin');

await userEvent.type(screen.getByLabelText('Name'), 'My Token');

await userEvent.click(createButton);

await waitFor(() =>
expect(assignMock).toHaveBeenCalledWith(
'/api-tokens/',
expect.objectContaining({
data: expect.objectContaining({
name: 'My Token',
scopes: expect.arrayContaining([
'project:read',
'project:write',
'project:admin',
'project:releases',
]),
}),
})
)
);
});

it('creates token without name', async function () {
MockApiClient.clearMockResponses();
const assignMock = MockApiClient.addMockResponse({
method: 'POST',
url: `/api-tokens/`,
});

render(<ApiNewToken />, {
context: RouterContextFixture(),
});
const createButton = screen.getByRole('button', {name: 'Create Token'});

const selectByValue = (name, value) =>
selectEvent.select(screen.getByRole('textbox', {name}), value);

await selectByValue('Project', 'Admin');
await selectByValue('Release', 'Admin');

await userEvent.click(createButton);

await waitFor(() =>
expect(assignMock).toHaveBeenCalledWith(
'/api-tokens/',
expect.objectContaining({
data: expect.objectContaining({
name: '', // expect a blank name
scopes: expect.arrayContaining([
'project:read',
'project:write',
'project:admin',
'project:releases',
]),
}),
})
)
);
});
});
44 changes: 31 additions & 13 deletions static/app/views/settings/account/apiNewToken.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {Component} from 'react';
import {browserHistory} from 'react-router';

import ApiForm from 'sentry/components/forms/apiForm';
import TextField from 'sentry/components/forms/fields/textField';
import ExternalLink from 'sentry/components/links/externalLink';
import Panel from 'sentry/components/panels/panel';
import PanelBody from 'sentry/components/panels/panelBody';
Expand All @@ -18,6 +19,7 @@ import PermissionSelection from 'sentry/views/settings/organizationDeveloperSett

const API_INDEX_ROUTE = '/settings/account/api/auth-tokens/';
type State = {
name: string | null;
newToken: NewInternalAppApiToken | null;
permissions: Permissions;
};
Expand All @@ -26,6 +28,7 @@ export default class ApiNewToken extends Component<{}, State> {
constructor(props: {}) {
super(props);
this.state = {
name: null,
permissions: {
Event: 'no-access',
Team: 'no-access',
Expand Down Expand Up @@ -75,12 +78,11 @@ export default class ApiNewToken extends Component<{}, State> {
handleGoBack={this.handleGoBack}
/>
) : (
<Panel>
<PanelHeader>{t('Permissions')}</PanelHeader>
<div>
<ApiForm
apiMethod="POST"
apiEndpoint="/api-tokens/"
initialData={{scopes: []}}
initialData={{scopes: [], name: ''}}
onSubmitSuccess={response => {
this.setState({newToken: response});
}}
Expand All @@ -94,17 +96,33 @@ export default class ApiNewToken extends Component<{}, State> {
)}
submitLabel={t('Create Token')}
>
<PanelBody>
<PermissionSelection
appPublished={false}
permissions={permissions}
onChange={value => {
this.setState({permissions: value});
}}
/>
</PanelBody>
<Panel>
<PanelHeader>{t('General')}</PanelHeader>
<PanelBody>
<TextField
name="name"
label={t('Name')}
help={t('A name to help you identify this token.')}
onChange={value => {
this.setState({name: value});
}}
/>
</PanelBody>
</Panel>
<Panel>
<PanelHeader>{t('Permissions')}</PanelHeader>
<PanelBody>
<PermissionSelection
appPublished={false}
permissions={permissions}
onChange={value => {
this.setState({permissions: value});
}}
/>
</PanelBody>
</Panel>
</ApiForm>
</Panel>
</div>
)}
</div>
</SentryDocumentTitle>
Expand Down
30 changes: 20 additions & 10 deletions static/app/views/settings/account/apiTokenRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,7 @@ function ApiTokenRow({token, onRemove, tokenPrefix = ''}: Props) {
return (
<StyledPanelItem>
<Controls>
<TokenPreview aria-label={t('Token preview')}>
{tokenPreview(
getDynamicText({
value: token.tokenLastCharacters,
fixed: 'ABCD',
}),
tokenPrefix
)}
</TokenPreview>
{token.name ? token.name : ''}
<ButtonWrapper>
<Button
data-test-id="token-delete"
Expand All @@ -41,6 +33,18 @@ function ApiTokenRow({token, onRemove, tokenPrefix = ''}: Props) {
</Controls>

<Details>
<TokenWrapper>
<Heading>{t('Token')}</Heading>
<TokenPreview aria-label={t('Token preview')}>
{tokenPreview(
getDynamicText({
value: token.tokenLastCharacters,
fixed: 'ABCD',
}),
tokenPrefix
)}
</TokenPreview>
</TokenWrapper>
<ScopesWrapper>
<Heading>{t('Scopes')}</Heading>
<ScopeList>{token.scopes.join(', ')}</ScopeList>
Expand Down Expand Up @@ -77,8 +81,14 @@ const Details = styled('div')`
margin-top: ${space(1)};
`;

const ScopesWrapper = styled('div')`
const TokenWrapper = styled('div')`
flex: 1;
margin-right: ${space(1)};
`;

const ScopesWrapper = styled('div')`
flex: 2;
margin-right: ${space(4)};
`;

const ScopeList = styled('div')`
Expand Down

0 comments on commit cc97408

Please sign in to comment.