diff --git a/apps/meteor/client/views/account/tokens/AccountTokensPage.js b/apps/meteor/client/views/account/tokens/AccountTokensPage.tsx similarity index 50% rename from apps/meteor/client/views/account/tokens/AccountTokensPage.js rename to apps/meteor/client/views/account/tokens/AccountTokensPage.tsx index da4f878977ec..ae594b7f6f48 100644 --- a/apps/meteor/client/views/account/tokens/AccountTokensPage.js +++ b/apps/meteor/client/views/account/tokens/AccountTokensPage.tsx @@ -1,21 +1,17 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; -import React from 'react'; +import React, { ReactElement } from 'react'; import Page from '../../../components/Page'; -import { useEndpointData } from '../../../hooks/useEndpointData'; import AccountTokensTable from './AccountTokensTable'; -import AddToken from './AddToken'; -const AccountTokensPage = () => { +const AccountTokensPage = (): ReactElement => { const t = useTranslation(); - const { value: data, reload } = useEndpointData('/v1/users.getPersonalAccessTokens'); return ( - - + ); diff --git a/apps/meteor/client/views/account/tokens/AccountTokensRow.tsx b/apps/meteor/client/views/account/tokens/AccountTokensRow.tsx deleted file mode 100644 index 188ca546ed7e..000000000000 --- a/apps/meteor/client/views/account/tokens/AccountTokensRow.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Button, ButtonGroup, Icon, Table } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; -import type { MomentInput } from 'moment'; -import React, { useCallback, FC } from 'react'; - -import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime'; - -type AccountTokensRowProps = { - bypassTwoFactor: unknown; - createdAt: MomentInput; - isMedium: boolean; - lastTokenPart: string; - name: string; - onRegenerate: (name: string) => void; - onRemove: (name: string) => void; -}; - -const AccountTokensRow: FC = ({ - bypassTwoFactor, - createdAt, - isMedium, - lastTokenPart, - name, - onRegenerate, - onRemove, -}) => { - const t = useTranslation(); - const formatDateAndTime = useFormatDateAndTime(); - const handleRegenerate = useCallback(() => onRegenerate(name), [name, onRegenerate]); - const handleRemove = useCallback(() => onRemove(name), [name, onRemove]); - - return ( - - - {name} - - {isMedium && {formatDateAndTime(createdAt)}} - ...{lastTokenPart} - {bypassTwoFactor ? t('Ignore') : t('Require')} - - - - - - - - ); -}; - -export default AccountTokensRow; diff --git a/apps/meteor/client/views/account/tokens/AccountTokensTable.js b/apps/meteor/client/views/account/tokens/AccountTokensTable.js deleted file mode 100644 index 70fb7f80aad6..000000000000 --- a/apps/meteor/client/views/account/tokens/AccountTokensTable.js +++ /dev/null @@ -1,140 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import { useSetModal, useToastMessageDispatch, useUserId, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useMemo, useCallback, useState } from 'react'; - -import GenericTable from '../../../components/GenericTable'; -import { useResizeInlineBreakpoint } from '../../../hooks/useResizeInlineBreakpoint'; -import AccountTokensRow from './AccountTokensRow'; -import InfoModal from './InfoModal'; - -const AccountTokensTable = ({ data, reload }) => { - const t = useTranslation(); - const dispatchToastMessage = useToastMessageDispatch(); - const setModal = useSetModal(); - - const userId = useUserId(); - - const regenerateToken = useMethod('personalAccessTokens:regenerateToken'); - const removeToken = useMethod('personalAccessTokens:removeToken'); - - const [ref, isMedium] = useResizeInlineBreakpoint([600], 200); - - const [params, setParams] = useState({ current: 0, itemsPerPage: 25 }); - - const tokensTotal = data && data.success ? data.tokens.length : 0; - - const { current, itemsPerPage } = params; - - const tokens = useMemo(() => { - if (!data) { - return null; - } - if (!data.success) { - return []; - } - const sliceStart = current > tokensTotal ? tokensTotal - itemsPerPage : current; - return data.tokens.slice(sliceStart, sliceStart + itemsPerPage); - }, [current, data, itemsPerPage, tokensTotal]); - - const closeModal = useCallback(() => setModal(null), [setModal]); - - const header = useMemo( - () => - [ - {t('API_Personal_Access_Token_Name')}, - isMedium && {t('Created_at')}, - {t('Last_token_part')}, - {t('Two Factor Authentication')}, - , - ].filter(Boolean), - [isMedium, t], - ); - - const onRegenerate = useCallback( - (name) => { - const onConfirm = async () => { - try { - setModal(null); - - const token = await regenerateToken({ tokenName: name }); - - setModal( - - } - confirmText={t('ok')} - onConfirm={closeModal} - />, - ); - - reload(); - } catch (e) { - setModal(null); - dispatchToastMessage({ type: 'error', message: e }); - } - }; - - setModal( - , - ); - }, - [closeModal, dispatchToastMessage, regenerateToken, reload, setModal, t, userId], - ); - - const onRemove = useCallback( - (name) => { - const onConfirm = async () => { - try { - await removeToken({ tokenName: name }); - - dispatchToastMessage({ type: 'success', message: t('Removed') }); - reload(); - closeModal(); - } catch (e) { - dispatchToastMessage({ type: 'error', message: e }); - } - }; - - setModal( - , - ); - }, - [closeModal, dispatchToastMessage, reload, removeToken, setModal, t], - ); - - return ( - - {useCallback( - (props) => ( - - ), - [isMedium, onRegenerate, onRemove], - )} - - ); -}; - -export default AccountTokensTable; diff --git a/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensRow.tsx b/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensRow.tsx new file mode 100644 index 000000000000..31477fa347a2 --- /dev/null +++ b/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensRow.tsx @@ -0,0 +1,46 @@ +import { IPersonalAccessToken, Serialized } from '@rocket.chat/core-typings'; +import { ButtonGroup, IconButton, TableRow, TableCell } from '@rocket.chat/fuselage'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import React, { useCallback, FC } from 'react'; + +import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime'; + +type AccountTokensRowProps = { + isMedium: boolean; + onRegenerate: (name: string) => void; + onRemove: (name: string) => void; +} & Serialized>; + +const AccountTokensRow: FC = ({ + bypassTwoFactor, + createdAt, + isMedium, + lastTokenPart, + name, + onRegenerate, + onRemove, +}) => { + const t = useTranslation(); + const formatDateAndTime = useFormatDateAndTime(); + const handleRegenerate = useCallback(() => onRegenerate(name), [name, onRegenerate]); + const handleRemove = useCallback(() => onRemove(name), [name, onRemove]); + + return ( + + + {name} + + {isMedium && {formatDateAndTime(createdAt)}} + ...{lastTokenPart} + {bypassTwoFactor ? t('Ignore') : t('Require')} + + + + + + + + ); +}; + +export default AccountTokensRow; diff --git a/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx b/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx new file mode 100644 index 000000000000..a08422285e20 --- /dev/null +++ b/apps/meteor/client/views/account/tokens/AccountTokensTable/AccountTokensTable.tsx @@ -0,0 +1,185 @@ +import { Box, Pagination, States, StatesAction, StatesActions, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage'; +import { useSetModal, useToastMessageDispatch, useUserId, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; +import React, { ReactElement, RefObject, useMemo, useCallback } from 'react'; + +import GenericModal from '../../../../components/GenericModal'; +import { + GenericTable, + GenericTableHeader, + GenericTableBody, + GenericTableLoadingTable, + GenericTableHeaderCell, +} from '../../../../components/GenericTable'; +import { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; +import { useEndpointData } from '../../../../hooks/useEndpointData'; +import { useResizeInlineBreakpoint } from '../../../../hooks/useResizeInlineBreakpoint'; +import { AsyncStatePhase } from '../../../../lib/asyncState'; +import AccountTokensRow from './AccountTokensRow'; +import AddToken from './AddToken'; + +const AccountTokensTable = (): ReactElement => { + const t = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + const setModal = useSetModal(); + const userId = useUserId(); + + const regenerateToken = useMethod('personalAccessTokens:regenerateToken'); + const removeToken = useMethod('personalAccessTokens:removeToken'); + const { value: data, phase, error, reload } = useEndpointData('/v1/users.getPersonalAccessTokens'); + + const [ref, isMedium] = useResizeInlineBreakpoint([600], 200) as [RefObject, boolean]; + + const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); + + const filteredTokens = useMemo(() => { + if (!data?.tokens) { + return null; + } + + const sliceStart = current > data?.tokens.length ? data?.tokens.length - itemsPerPage : current; + return data?.tokens.slice(sliceStart, sliceStart + itemsPerPage); + }, [current, data?.tokens, itemsPerPage]); + + const closeModal = useCallback(() => setModal(null), [setModal]); + + const headers = useMemo( + () => + [ + {t('API_Personal_Access_Token_Name')}, + isMedium && {t('Created_at')}, + {t('Last_token_part')}, + {t('Two Factor Authentication')}, + , + ].filter(Boolean), + [isMedium, t], + ); + + const handleRegenerate = useCallback( + (name) => { + const onConfirm: () => Promise = async () => { + try { + setModal(null); + const token = await regenerateToken({ tokenName: name }); + + setModal( + + + , + ); + + reload(); + } catch (error) { + setModal(null); + dispatchToastMessage({ type: 'error', message: error as Error }); + } + }; + + setModal( + + {t('API_Personal_Access_Tokens_Regenerate_Modal')} + , + ); + }, + [closeModal, dispatchToastMessage, regenerateToken, reload, setModal, t, userId], + ); + + const handleRemove = useCallback( + (name) => { + const onConfirm: () => Promise = async () => { + try { + await removeToken({ tokenName: name }); + dispatchToastMessage({ type: 'success', message: t('Token_has_been_removed') }); + reload(); + closeModal(); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error as Error }); + } + }; + + setModal( + + {t('API_Personal_Access_Tokens_Remove_Modal')} + , + ); + }, + [closeModal, dispatchToastMessage, reload, removeToken, setModal, t], + ); + + if (phase === AsyncStatePhase.REJECTED) { + return ( + + + + {t('Something_Went_Wrong')} + {t('We_Could_not_retrive_any_data')} + {error?.message} + + {t('Retry')} + + + + ); + } + + return ( + <> + + {phase === AsyncStatePhase.LOADING && ( + + {headers} + {phase === AsyncStatePhase.LOADING && } + + )} + {filteredTokens && filteredTokens?.length > 0 && phase === AsyncStatePhase.RESOLVED && ( + <> + + {headers} + + {phase === AsyncStatePhase.RESOLVED && + filteredTokens && + filteredTokens.map((filteredToken) => ( + + ))} + + + + + )} + {phase === AsyncStatePhase.RESOLVED && filteredTokens?.length === 0 && ( + + + {t('No_results_found')} + + )} + + ); +}; + +export default AccountTokensTable; diff --git a/apps/meteor/client/views/account/tokens/AddToken.js b/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx similarity index 54% rename from apps/meteor/client/views/account/tokens/AddToken.js rename to apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx index dbcdbcae0a0f..dc695fc8eb06 100644 --- a/apps/meteor/client/views/account/tokens/AddToken.js +++ b/apps/meteor/client/views/account/tokens/AccountTokensTable/AddToken.tsx @@ -1,77 +1,65 @@ import { Box, TextInput, Button, Field, FieldGroup, Margins, CheckBox } from '@rocket.chat/fuselage'; import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useSetModal, useToastMessageDispatch, useUserId, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; -import React, { useCallback } from 'react'; +import React, { ReactElement, useCallback } from 'react'; -import { useForm } from '../../../hooks/useForm'; -import InfoModal from './InfoModal'; +import GenericModal from '../../../../components/GenericModal'; +import { useForm } from '../../../../hooks/useForm'; const initialValues = { name: '', bypassTwoFactor: false, }; -const AddToken = ({ onDidAddToken, ...props }) => { +const AddToken = ({ reload, ...props }: { reload: () => void }): ReactElement => { const t = useTranslation(); + const userId = useUserId(); const createTokenFn = useMethod('personalAccessTokens:generateToken'); const dispatchToastMessage = useToastMessageDispatch(); + const bypassTwoFactorCheckboxId = useUniqueId(); const setModal = useSetModal(); - const userId = useUserId(); - const { values, handlers, reset } = useForm(initialValues); - - const { name, bypassTwoFactor } = values; + const { name, bypassTwoFactor } = values as typeof initialValues; const { handleName, handleBypassTwoFactor } = handlers; - const closeModal = useCallback(() => setModal(null), [setModal]); - - const handleAdd = useCallback(async () => { + const handleAddToken = useCallback(async () => { try { const token = await createTokenFn({ tokenName: name, bypassTwoFactor }); setModal( - - } - confirmText={t('ok')} - onConfirm={closeModal} - />, + setModal(null)}> + + , ); reset(); - onDidAddToken(); + reload(); } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); + dispatchToastMessage({ type: 'error', message: error as Error }); } - }, [bypassTwoFactor, closeModal, createTokenFn, dispatchToastMessage, name, onDidAddToken, reset, setModal, t, userId]); - - const bypassTwoFactorCheckboxId = useUniqueId(); + }, [bypassTwoFactor, createTokenFn, dispatchToastMessage, name, reload, reset, setModal, t, userId]); return ( - + - - - {t('Ignore')} {t('Two Factor Authentication')} - + {t('Ignore_Two_Factor_Authentication')} diff --git a/apps/meteor/client/views/account/tokens/AccountTokensTable/index.ts b/apps/meteor/client/views/account/tokens/AccountTokensTable/index.ts new file mode 100644 index 000000000000..0643bb37626d --- /dev/null +++ b/apps/meteor/client/views/account/tokens/AccountTokensTable/index.ts @@ -0,0 +1 @@ +export { default } from './AccountTokensTable'; diff --git a/apps/meteor/client/views/account/tokens/InfoModal.tsx b/apps/meteor/client/views/account/tokens/InfoModal.tsx deleted file mode 100644 index 25e473ad49e2..000000000000 --- a/apps/meteor/client/views/account/tokens/InfoModal.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Button, ButtonGroup, Modal } from '@rocket.chat/fuselage'; -import React, { FC, ReactNode } from 'react'; - -type InfoModalProps = { - title: string; - content: ReactNode; - icon: ReactNode; - confirmText: string; - cancelText: string; - onConfirm: () => void; - onClose: () => void; -}; - -const InfoModal: FC = ({ title, content, icon, confirmText, cancelText, onConfirm, onClose, ...props }) => ( - - - {icon} - {title} - - - {content} - - - {cancelText && } - {confirmText && onConfirm && ( - - )} - - - -); - -export default InfoModal; diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 74df500ad0fd..94a55cb88d6b 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -2313,6 +2313,7 @@ "Iframe_X_Frame_Options_Description": "Options to X-Frame-Options. [You can see all the options here.](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options#Syntax)", "Ignore": "Ignore", "Ignored": "Ignored", + "Ignore_Two_Factor_Authentication": "Ignore Two Factor Authentication", "Images": "Images", "IMAP_intercepter_already_running": "IMAP intercepter already running", "IMAP_intercepter_Not_running": "IMAP intercepter Not running", @@ -4565,6 +4566,7 @@ "Token": "Token", "Token_Access": "Token Access", "Token_Controlled_Access": "Token Controlled Access", + "Token_has_been_removed": "Token has been removed", "Token_required": "Token required", "Tokens_Minimum_Needed_Balance": "Minimum needed token balance", "Tokens_Minimum_Needed_Balance_Description": "Set minimum needed balance on each token. Blank or \"0\" for not limit.", diff --git a/apps/meteor/tests/e2e/10-user-preferences.spec.ts b/apps/meteor/tests/e2e/10-user-preferences.spec.ts index ee2678025663..bf41ad363be3 100644 --- a/apps/meteor/tests/e2e/10-user-preferences.spec.ts +++ b/apps/meteor/tests/e2e/10-user-preferences.spec.ts @@ -3,7 +3,7 @@ import { faker } from '@faker-js/faker'; import { test, expect } from './utils/test'; import { Auth, HomeChannel, AccountProfile } from './page-objects'; -test.describe('User preferences', () => { +test.describe('My Account', () => { let pageAuth: Auth; let pageHomeChannel: HomeChannel; let pageAccountProfile: AccountProfile; @@ -11,39 +11,80 @@ test.describe('User preferences', () => { const newName = faker.name.findName(); const newUsername = faker.internet.userName(newName); + const token = faker.random.alpha(10); + test.beforeEach(async ({ page }) => { pageAuth = new Auth(page); pageHomeChannel = new HomeChannel(page); pageAccountProfile = new AccountProfile(page); }); - test.beforeEach(async () => { - await pageAuth.doLogin(); - }); + test.describe('User Profile', () => { + test.beforeEach(async () => { + await pageAuth.doLogin(); + }); - test('expect update profile with new name and username', async ({ page }) => { - await pageHomeChannel.sidenav.doOpenProfile(); + test('expect update profile with new name and username', async ({ page }) => { + await pageHomeChannel.sidenav.doOpenProfile(); - await pageAccountProfile.inputName.fill(newName); - await pageAccountProfile.inputUsername.fill(newUsername); - await pageAccountProfile.btnSubmit.click(); - await page.goto('/'); - }); + await pageAccountProfile.inputName.fill(newName); + await pageAccountProfile.inputUsername.fill(newUsername); + await pageAccountProfile.btnSubmit.click(); + await page.goto('/'); + }); + + test('expect show new username in the last message', async () => { + await pageHomeChannel.sidenav.doOpenChat('general'); + await pageHomeChannel.content.doSendMessage('any_message'); + + await expect(pageHomeChannel.content.lastUserMessageNotSequential).toContainText(newUsername); + }); - test('expect show new username in the last message', async () => { - await pageHomeChannel.sidenav.doOpenChat('general'); - await pageHomeChannel.content.doSendMessage('any_message'); + test('expect show new username in card and profile', async () => { + await pageHomeChannel.sidenav.doOpenChat('general'); + await pageHomeChannel.content.doSendMessage('any_message'); - await expect(pageHomeChannel.content.lastUserMessageNotSequential).toContainText(newUsername); + await pageHomeChannel.content.lastUserMessageNotSequential.locator('figure').click(); + await pageHomeChannel.content.userCardLinkProfile.click(); + + await expect(pageHomeChannel.tabs.userInfoUsername).toHaveText(newUsername); + }); }); - test('expect show new username in card and profile', async () => { - await pageHomeChannel.sidenav.doOpenChat('general'); - await pageHomeChannel.content.doSendMessage('any_message'); + test.describe('Personal Access Tokens', () => { + test.beforeEach(async () => { + await pageAuth.doLogin(); + await pageHomeChannel.sidenav.doOpenProfile(); + await pageAccountProfile.sidenav.linkTokens.click(); + }); + + test('expect show empty personal access tokens table', async () => { + await expect(pageAccountProfile.tokensTableEmpty).toBeVisible(); + await expect(pageAccountProfile.inputToken).toBeVisible(); + }); + + test('expect show new personal token', async () => { + await pageAccountProfile.inputToken.type(token); + await pageAccountProfile.btnTokensAdd.click(); + await expect(pageAccountProfile.tokenAddedModal).toBeVisible(); + }); + + test('expect not allow add new personal token with same name', async ({ page }) => { + await pageAccountProfile.inputToken.type(token); + await pageAccountProfile.btnTokensAdd.click(); + await expect(page.locator('.rcx-toastbar.rcx-toastbar--error')).toBeVisible(); + }); - await pageHomeChannel.content.lastUserMessageNotSequential.locator('figure').click(); - await pageHomeChannel.content.userCardLinkProfile.click(); + test('expect regenerate personal token', async () => { + await pageAccountProfile.tokenInTable(token).locator('button >> nth=0').click(); + await pageAccountProfile.btnRegenerateTokenModal.click(); + await expect(pageAccountProfile.tokenAddedModal).toBeVisible(); + }); - await expect(pageHomeChannel.tabs.userInfoUsername).toHaveText(newUsername); + test('expect delete personal token', async ({ page }) => { + await pageAccountProfile.tokenInTable(token).locator('button >> nth=1').click(); + await pageAccountProfile.btnRemoveTokenModal.click(); + await expect(page.locator('.rcx-toastbar.rcx-toastbar--success')).toBeVisible(); + }); }); }); diff --git a/apps/meteor/tests/e2e/page-objects/account-profile.ts b/apps/meteor/tests/e2e/page-objects/account-profile.ts index 5e951a1d64ba..ecf167e266bb 100644 --- a/apps/meteor/tests/e2e/page-objects/account-profile.ts +++ b/apps/meteor/tests/e2e/page-objects/account-profile.ts @@ -1,10 +1,15 @@ import { Locator, Page } from '@playwright/test'; +import { AccountSidenav } from './fragments/account-sidenav'; + export class AccountProfile { private readonly page: Page; + readonly sidenav: AccountSidenav; + constructor(page: Page) { this.page = page; + this.sidenav = new AccountSidenav(page); } get inputName(): Locator { @@ -26,4 +31,32 @@ export class AccountProfile { get emailTextInput(): Locator { return this.page.locator('//label[contains(text(), "Email")]/..//input'); } + + get inputToken(): Locator { + return this.page.locator('[data-qa="PersonalTokenField"]'); + } + + get tokensTableEmpty(): Locator { + return this.page.locator('//div[contains(text(), "No results found")]'); + } + + get btnTokensAdd(): Locator { + return this.page.locator('//button[contains(text(), "Add")]'); + } + + get tokenAddedModal(): Locator { + return this.page.locator("//div[text()='Personal Access Token successfully generated']"); + } + + tokenInTable(name: string): Locator { + return this.page.locator(`tr[qa-token-name="${name}"]`); + } + + get btnRegenerateTokenModal(): Locator { + return this.page.locator('//button[contains(text(), "Regenerate token")]'); + } + + get btnRemoveTokenModal(): Locator { + return this.page.locator('//button[contains(text(), "Remove")]'); + } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/account-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/account-sidenav.ts new file mode 100644 index 000000000000..714adab52a1a --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/account-sidenav.ts @@ -0,0 +1,13 @@ +import { Locator, Page } from '@playwright/test'; + +export class AccountSidenav { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + get linkTokens(): Locator { + return this.page.locator('.flex-nav [href="/account/tokens"]'); + } +} diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index b4a5c5e7ed2f..89bcd0f2b7bf 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -16,7 +16,7 @@ export interface IPersonalAccessToken extends ILoginToken { type: 'personalAccessToken'; createdAt: Date; lastTokenPart: string; - name?: string; + name: string; bypassTwoFactor?: boolean; } diff --git a/packages/rest-typings/src/v1/users.ts b/packages/rest-typings/src/v1/users.ts index 1b07ad614ff7..ca42e250f5d8 100644 --- a/packages/rest-typings/src/v1/users.ts +++ b/packages/rest-typings/src/v1/users.ts @@ -1,4 +1,4 @@ -import type { IExportOperation, ISubscription, ITeam, IUser } from '@rocket.chat/core-typings'; +import type { IExportOperation, ISubscription, ITeam, IUser, IPersonalAccessToken } from '@rocket.chat/core-typings'; import Ajv from 'ajv'; import type { UserCreateParamsPOST } from './users/UserCreateParamsPOST'; @@ -104,6 +104,8 @@ export type UserPresence = Readonly< Partial> & Required> >; +export type UserPersonalTokens = Pick & { createdAt: string }; + export type UsersEndpoints = { '/v1/users.2fa.enableEmail': { POST: () => void; @@ -193,12 +195,7 @@ export type UsersEndpoints = { '/v1/users.getPersonalAccessTokens': { GET: () => { - tokens: { - name?: string; - createdAt: string; - lastTokenPart: string; - bypassTwoFactor: boolean; - }[]; + tokens: UserPersonalTokens[]; }; }; '/v1/users.regeneratePersonalAccessToken': {