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
11 changes: 0 additions & 11 deletions api/v4/source/system.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1013,20 +1013,9 @@

No permission required but having the `manage_system` permission returns more information.
operationId: GetClientLicense
parameters:
- name: format
in: query
required: true
description: Must be `old`, other formats not implemented yet
schema:
type: string
responses:
"200":
description: License retrieval successful
"400":
$ref: "#/components/responses/BadRequest"
"501":
$ref: "#/components/responses/NotImplemented"
/api/v4/license/load_metric:
get:
tags:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ describe('Self hosted Pricing modal', () => {
}

function withTrialLicense(trial: string) {
cy.intercept('GET', '**/api/v4/license/client?format=old', {
cy.intercept('GET', '**/api/v4/license/client', {
statusCode: 200,
body: {
IsLicensed: 'true',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import * as TIMEOUTS from '@/fixtures/timeouts';

const OLD_LICENSE_ID = 'old-license-id-0000000000000';

// Current (already applied) license, as returned by GET /license/client?format=old.
// Current (already applied) license, as returned by GET /license/client.
const currentClientLicense = buildClientLicense({
id: OLD_LICENSE_ID,
skuName: 'Professional',
Expand Down Expand Up @@ -57,7 +57,7 @@ describe('System console - License preview/diff view', () => {
// mutable holder so we can simulate the server only reporting the new
// license once propagation completes (by the time the modal closes).
const clientLicenseHolder = {body: currentClientLicense};
cy.intercept('GET', '**/api/v4/license/client?format=old', (req) => {
cy.intercept('GET', '**/api/v4/license/client', (req) => {
req.reply({statusCode: 200, body: clientLicenseHolder.body});
}).as('getClientLicense');

Expand Down Expand Up @@ -121,7 +121,7 @@ describe('System console - License preview/diff view', () => {

it('MM-67113 - Warns when re-uploading the currently applied license and leaves it unchanged', () => {
// # The displayed license stays Professional throughout this flow
cy.intercept('GET', '**/api/v4/license/client?format=old', {
cy.intercept('GET', '**/api/v4/license/client', {
statusCode: 200,
body: currentClientLicense,
}).as('getClientLicense');
Expand Down Expand Up @@ -222,7 +222,7 @@ function buildLicense({id, skuName, skuShortName, users}) {
};
}

// Build a ClientLicense object (old format) matching GET /license/client?format=old.
// Build a ClientLicense object (old format) matching GET /license/client.
// All values are strings, as produced by the server.
function buildClientLicense({id, skuName, skuShortName, users}) {
const now = Date.now();
Expand Down
2 changes: 1 addition & 1 deletion e2e-tests/cypress/tests/support/api/system.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function hasLicenseForFeature(license, key) {
}

Cypress.Commands.add('apiGetClientLicense', () => {
return cy.request('/api/v4/license/client?format=old').then((response) => {
return cy.request('/api/v4/license/client').then((response) => {
expect(response.status).to.equal(200);

const license = response.body;
Expand Down
12 changes: 0 additions & 12 deletions server/channels/api4/license.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,6 @@ func (api *API) InitLicense() {
}

func getClientLicense(c *Context, w http.ResponseWriter, r *http.Request) {
format := r.URL.Query().Get("format")

if format == "" {
c.Err = model.NewAppError("getClientLicense", "api.license.client.old_format.app_error", nil, "", http.StatusBadRequest)
return
}

if format != "old" {
c.SetInvalidParam("format")
return
}

var clientLicense map[string]string

if c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionReadLicenseInformation) {
Expand Down
12 changes: 0 additions & 12 deletions server/channels/api4/license_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,6 @@ func localRemoveLicense(c *Context, w http.ResponseWriter, r *http.Request) {
}

func localGetClientLicense(c *Context, w http.ResponseWriter, r *http.Request) {
format := r.URL.Query().Get("format")

if format == "" {
c.Err = model.NewAppError("localGetClientLicense", "api.license.client.old_format.app_error", nil, "", http.StatusBadRequest)
return
}

if format != "old" {
c.SetInvalidParam("format")
return
}

clientLicense := c.App.Srv().ClientLicense()

if _, err := w.Write([]byte(model.MapToJSON(clientLicense))); err != nil {
Expand Down
10 changes: 4 additions & 6 deletions server/channels/api4/license_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,12 @@ func TestGetOldClientLicense(t *testing.T) {
require.NoError(t, err)

resp, err := client.DoAPIGet(context.Background(), "/license/client", "")
require.Error(t, err, "get /license/client did not return an error")
require.Equal(t, http.StatusBadRequest, resp.StatusCode,
"expected 400 bad request")
require.NoError(t, err, "get /license/client should not return an error")
require.Equal(t, http.StatusOK, resp.StatusCode, "expected 200 OK")

resp, err = client.DoAPIGet(context.Background(), "/license/client?format=junk", "")
require.Error(t, err, "get /license/client?format=junk did not return an error")
require.Equal(t, http.StatusBadRequest, resp.StatusCode,
"expected 400 Bad Request")
require.NoError(t, err, "get /license/client?format=junk should not return an error")
require.Equal(t, http.StatusOK, resp.StatusCode, "expected 200 OK")

license, _, err = th.SystemAdminClient.GetOldClientLicense(context.Background(), "")
require.NoError(t, err)
Expand Down
4 changes: 0 additions & 4 deletions server/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2700,10 +2700,6 @@
"id": "api.license.add_license.wrong_environment_test.app_error",
"translation": "This is a test or development license, but this server is running in a production environment. Test or development licenses can only be used on test or development servers."
},
{
"id": "api.license.client.old_format.app_error",
"translation": "New format for the client license is not supported yet. Please specify format=old in the query string."
},
{
"id": "api.license.load_metric.app_error",
"translation": "Failed to compute monthly active users."
Expand Down
26 changes: 26 additions & 0 deletions webapp/channels/src/components/integrations/bots/bot.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -296,4 +296,30 @@ describe('components/integrations/bots/Bot', () => {
expect(screen.queryByText(/^Disable$/)).not.toBeInTheDocument();
expect(screen.getByText(/^Enable$/)).toBeInTheDocument();
});

it('shows a copy button for a newly created token secret', async () => {
const bot = UtilsTestHelper.getBotMock({user_id: '1', owner_id: '1'});
const owner = UtilsTestHelper.getUserMock({id: bot.owner_id});
const user = UtilsTestHelper.getUserMock({id: bot.user_id});
const createUserAccessToken = jest.fn().mockResolvedValue({data: {id: 'new-token-id', description: 'bot token', token: 'bot-secret'}});

renderWithContext(
<Bot
bot={bot}
owner={owner}
user={user}
accessTokens={{}}
team={team}
actions={{...actions, createUserAccessToken}}
fromApp={false}
/>,
);

fireEvent.click(screen.getByText('Create New Token'));
fireEvent.change(screen.getByLabelText('Token Description:'), {target: {value: 'bot token'}});
fireEvent.click(screen.getByText('Save'));

expect(await screen.findByText(/bot-secret/)).toBeInTheDocument();
expect(screen.getByLabelText('Copy Token')).toBeInTheDocument();
});
});
9 changes: 8 additions & 1 deletion webapp/channels/src/components/integrations/bots/bot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import React from 'react';
import type {ChangeEvent, SyntheticEvent, ReactNode} from 'react';
import {FormattedMessage} from 'react-intl';
import {defineMessage, FormattedMessage} from 'react-intl';
import {Link} from 'react-router-dom';

import {Button} from '@mattermost/shared/components/button';
Expand All @@ -14,12 +14,15 @@ import type {UserProfile, UserAccessToken} from '@mattermost/types/users';
import type {ActionResult} from 'mattermost-redux/types/actions';

import ConfirmModal from 'components/confirm_modal';
import CopyText from 'components/copy_text';
import Markdown from 'components/markdown';
import SaveButton from 'components/save_button';
import WarningIcon from 'components/widgets/icons/fa_warning_icon';

import * as Utils from 'utils/utils';

const copyTokenMessage = defineMessage({id: 'integrations.copy_token', defaultMessage: 'Copy Token'});

export function matchesFilter(bot: BotType, filter?: string, owner?: UserProfile): boolean {
if (!filter) {
return true;
Expand Down Expand Up @@ -471,6 +474,10 @@ export default class Bot extends React.PureComponent<Props, State> {
/>
{this.state.token.token}
</strong>
<CopyText
label={copyTokenMessage}
value={this.state.token.token}
/>
<div className='mt-2'>
<Button
emphasis='primary'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,20 @@ describe('UserAccessTokenSection component', () => {
expect(props.actions.createUserAccessToken).not.toHaveBeenCalled();
});

test('shows a copy button for a newly created token secret', async () => {
const createUserAccessToken = jest.fn().mockResolvedValue({data: {id: 'created-token-id', description: 'my token', is_active: true, token: 'created-secret'}});
const {container} = renderSection({
actions: {...getBaseProps().actions, createUserAccessToken},
});

startCreating();
change(container, '#newTokenDescription', 'my token');
clickSave();

expect(await screen.findByText(/created-secret/)).toBeInTheDocument();
expect(screen.getByLabelText('Copy Token')).toBeInTheDocument();
});

test('surfaces the expiry error inline and disables Save when a custom date is cleared, without clicking Save', () => {
const {container} = renderSection();
startCreating();
Expand Down Expand Up @@ -375,6 +389,7 @@ describe('UserAccessTokenSection component', () => {

expect(setRequireConfirm).toHaveBeenCalledWith(true, expect.any(Function));
expect(await screen.findByText(/new-secret/)).toBeInTheDocument();
expect(screen.getByLabelText('Copy Token')).toBeInTheDocument();
expect(rotateUserAccessToken).toHaveBeenCalledWith('t1', undefined);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// See LICENSE.txt for license information.

import React from 'react';
import {FormattedDate, FormattedMessage, FormattedTime, injectIntl} from 'react-intl';
import {defineMessage, FormattedDate, FormattedMessage, FormattedTime, injectIntl} from 'react-intl';
import type {WrappedComponentProps} from 'react-intl';

import {Button} from '@mattermost/shared/components/button';
Expand All @@ -14,6 +14,7 @@ import type {ActionResult} from 'mattermost-redux/types/actions';
import * as UserUtils from 'mattermost-redux/utils/user_utils';

import ConfirmModal from 'components/confirm_modal';
import CopyText from 'components/copy_text';
import ExternalLink from 'components/external_link';
import SaveButton from 'components/save_button';
import SettingItemMax from 'components/setting_item_max';
Expand All @@ -28,6 +29,7 @@ const SECTION_TOKENS = 'tokens';
const TOKEN_CREATING = 'creating';
const TOKEN_CREATED = 'created';
const TOKEN_NOT_CREATING = 'not_creating';
const copyTokenMessage = defineMessage({id: 'integrations.copy_token', defaultMessage: 'Copy Token'});

const APPROACHING_EXPIRY_DAYS = 7;
const MS_PER_DAY = 24 * 60 * 60 * 1000;
Expand Down Expand Up @@ -500,13 +502,19 @@ class UserAccessTokenSection extends React.PureComponent<Props, State> {
/>
</div>
) : (
<strong className='word-break--all'>
<FormattedMessage
id='user.settings.tokens.token'
defaultMessage='Access Token: '
<>
<strong className='word-break--all'>
<FormattedMessage
id='user.settings.tokens.token'
defaultMessage='Access Token: '
/>
{state.newToken!.token}
</strong>
<CopyText
label={copyTokenMessage}
value={state.newToken!.token!}
/>
{state.newToken!.token}
</strong>
</>
)}
</div>
),
Expand Down Expand Up @@ -1130,6 +1138,10 @@ class UserAccessTokenSection extends React.PureComponent<Props, State> {
/>
{this.state.newToken!.token}
</strong>
<CopyText
label={copyTokenMessage}
value={this.state.newToken!.token!}
/>
</div>
);
} else {
Expand Down
7 changes: 7 additions & 0 deletions webapp/channels/src/sass/components/_post.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1770,11 +1770,18 @@
.post__img {
width: 24px;
height: 24px;
flex: 0 0 auto;
padding: 0;
text-align: left;

img.avatar-post-preview {
display: block;

// The shared avatar rules set min-width: 0, which lets the image
// shrink (and look squished) inside constrained flex layouts.
// Pin it to its intended size so it always stays round.
min-width: 24px;
flex-shrink: 0;
}
}

Expand Down
2 changes: 1 addition & 1 deletion webapp/platform/client/src/client4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3068,7 +3068,7 @@ export default class Client4 {

getClientLicenseOld = () => {
return this.doFetch<ClientLicense>(
`${this.getBaseRoute()}/license/client?format=old`,
`${this.getBaseRoute()}/license/client`,
{method: 'get'},
);
};
Expand Down
Loading