Regression: fix apps path (#25809)

pull/25819/head
Guilherme Gazzo 4 years ago committed by GitHub
parent 993745d399
commit c27412b0bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 28
      apps/meteor/app/api/server/v1/misc.ts
  2. 19
      apps/meteor/app/apps/client/@types/IOrchestrator.ts
  3. 157
      apps/meteor/app/apps/client/orchestrator.ts
  4. 8
      apps/meteor/client/lib/meteorCallWrapper.ts
  5. 27
      apps/meteor/client/lib/userData.ts
  6. 8
      apps/meteor/client/providers/ServerProvider.tsx
  7. 4
      apps/meteor/client/views/admin/apps/AppDetailsPage.tsx
  8. 3
      apps/meteor/client/views/admin/apps/hooks/useCategories.ts
  9. 1
      packages/core-typings/src/IUser.ts
  10. 63
      packages/rest-typings/src/apps/index.ts
  11. 71
      packages/rest-typings/src/v1/me.ts
  12. 28
      packages/rest-typings/src/v1/misc.ts

@ -14,6 +14,7 @@ import {
isMethodCallAnonProps,
isMeteorCall,
} from '@rocket.chat/rest-typings';
import { IUser } from '@rocket.chat/core-typings';
import { hasPermission } from '../../../authorization/server';
import { Users } from '../../../models/server';
@ -166,17 +167,24 @@ API.v1.addRoute(
'me',
{ authRequired: true },
{
get() {
async get() {
const fields = getDefaultUserFields();
const user = Users.findOneById(this.userId, { fields });
// The password hash shouldn't be leaked but the client may need to know if it exists.
if (user?.services?.password?.bcrypt) {
user.services.password.exists = true;
delete user.services.password.bcrypt;
}
return API.v1.success(this.getUserInfo(user));
const { services, ...user } = Users.findOneById(this.userId, { fields }) as IUser;
return API.v1.success(
this.getUserInfo({
...user,
...(services && {
services: {
...services,
password: {
// The password hash shouldn't be leaked but the client may need to know if it exists.
exists: Boolean(services?.password?.bcrypt),
} as any,
},
}),
}),
);
},
},
);

@ -1,4 +1,3 @@
import { IAppInfo } from '@rocket.chat/apps-engine/definition/metadata/IAppInfo';
import { ISetting } from '@rocket.chat/apps-engine/definition/settings/ISetting';
export interface IDetailedDescription {
@ -148,7 +147,6 @@ export interface IAppLanguage {
export interface IAppExternalURL {
url: string;
success: boolean;
}
export interface ICategory {
@ -159,10 +157,10 @@ export interface ICategory {
title: string;
}
export interface IDeletedInstalledApp {
app: IAppInfo;
success: boolean;
}
// export interface IDeletedInstalledApp {
// app: IAppInfo;
// success: boolean;
// }
export interface IAppSynced {
app: IAppFromMarketplace;
@ -193,12 +191,3 @@ export interface ISettingsReturn {
settings: ISettings;
success: boolean;
}
export interface ISettingsPayload {
settings: ISetting[];
}
export interface ISettingsSetReturn {
updated: ISettings;
success: boolean;
}

@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import type { ISetting } from '@rocket.chat/apps-engine/definition/settings';
import { AppClientManager } from '@rocket.chat/apps-engine/client/AppClientManager';
import { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api';
import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus';
@ -6,6 +7,7 @@ import { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPe
import { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage/IAppStorageItem';
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { AppScreenshot, Serialized } from '@rocket.chat/core-typings';
import { App } from '../../../client/views/admin/apps/types';
import { dispatchToastMessage } from '../../../client/lib/toast';
@ -15,27 +17,23 @@ import { createDeferredValue } from '../lib/misc/DeferredValue';
import {
IPricingPlan,
EAppPurchaseType,
IAppFromMarketplace,
// IAppFromMarketplace,
IAppLanguage,
IAppExternalURL,
ICategory,
IDeletedInstalledApp,
IAppSynced,
IAppScreenshots,
// IAppSynced,
// IAppScreenshots,
// IScreenshot,
IAuthor,
IDetailedChangelog,
IDetailedDescription,
ISubscriptionInfo,
ISettingsReturn,
ISettingsPayload,
ISettingsSetReturn,
} from './@types/IOrchestrator';
import { AppWebsocketReceiver } from './communication';
import { handleI18nResources } from './i18n';
import { RealAppsEngineUIHost } from './RealAppsEngineUIHost';
const { APIClient } = require('../../utils');
const { hasAtLeastOnePermission } = require('../../authorization');
import { APIClient } from '../../utils/client';
import { hasAtLeastOnePermission } from '../../authorization/client';
export interface IAppsFromMarketplace {
price: number;
@ -123,8 +121,9 @@ class AppClientOrchestrator {
}
}
public screenshots(appId: string): IAppScreenshots {
return APIClient.get(`/v1/apps/${appId}/screenshots`);
public async screenshots(appId: string): Promise<AppScreenshot[]> {
const { screenshots } = await APIClient.get(`/apps/${appId}/screenshots`);
return screenshots;
}
public isEnabled(): Promise<boolean> | undefined {
@ -132,79 +131,87 @@ class AppClientOrchestrator {
}
public async getApps(): Promise<App[]> {
const { apps } = await APIClient.get('/v1/apps');
return apps;
const result = await APIClient.get('/apps');
if ('apps' in result) {
return result.apps;
}
throw new Error('Apps not found');
}
public async getAppsFromMarketplace(): Promise<IAppsFromMarketplace[]> {
const appsOverviews: IAppFromMarketplace[] = await APIClient.get('/v1/apps', { marketplace: 'true' });
return appsOverviews.map((app: IAppFromMarketplace) => {
const { latest, price, pricingPlans, purchaseType, isEnterpriseOnly, modifiedAt } = app;
return {
...latest,
price,
pricingPlans,
purchaseType,
isEnterpriseOnly,
modifiedAt,
};
});
public async getAppsFromMarketplace(): Promise<App[]> {
const result = await APIClient.get('/apps', { marketplace: 'true' });
if ('apps' in result) {
const { apps: appsOverviews } = result;
return appsOverviews.map((app) => {
const { latest, price, pricingPlans, purchaseType, isEnterpriseOnly, modifiedAt } = app;
return {
...latest,
price,
pricingPlans,
purchaseType,
isEnterpriseOnly,
modifiedAt,
};
});
}
throw new Error('Apps not found');
}
public async getAppsOnBundle(bundleId: string): Promise<App[]> {
const { apps } = await APIClient.get(`/v1/apps/bundles/${bundleId}/apps`);
const { apps } = await APIClient.get(`/apps/bundles/${bundleId}/apps`);
return apps;
}
public async getAppsLanguages(): Promise<IAppLanguage> {
const { apps } = await APIClient.get('/v1/apps/languages');
const { apps } = await APIClient.get('/apps/languages');
return apps;
}
public async getApp(appId: string): Promise<App> {
const { app } = await APIClient.get(`/v1/apps/${appId}`);
const { app } = await APIClient.get(`/apps/${appId}` as any);
return app;
}
public async getAppFromMarketplace(appId: string, version: string): Promise<App> {
const { app } = await APIClient.get(`/v1/apps/${appId}`, {
marketplace: 'true',
version,
});
return app;
const result = await APIClient.get(
`/apps/${appId}` as any,
{
marketplace: 'true',
version,
} as any,
);
return result;
}
public async getLatestAppFromMarketplace(appId: string, version: string): Promise<App> {
const { app } = await APIClient.get(`/v1/apps/${appId}`, {
marketplace: 'true',
update: 'true',
appVersion: version,
});
const { app } = await APIClient.get(
`/apps/${appId}` as any,
{
marketplace: 'true',
update: 'true',
appVersion: version,
} as any,
);
return app;
}
public async getAppSettings(appId: string): Promise<ISettingsReturn> {
const { settings } = await APIClient.get(`/v1/apps/${appId}/settings`);
return settings;
}
public async setAppSettings(appId: string, settings: ISettingsPayload): Promise<ISettingsSetReturn> {
const { updated } = await APIClient.post(`/v1/apps/${appId}/settings`, undefined, { settings });
return updated;
public async setAppSettings(appId: string, settings: ISetting[]): Promise<void> {
await APIClient.post(`/apps/${appId}/settings`, { settings });
}
public async getAppApis(appId: string): Promise<IApiEndpointMetadata[]> {
const { apis } = await APIClient.get(`/v1/apps/${appId}/apis`);
const { apis } = await APIClient.get(`/apps/${appId}/apis`);
return apis;
}
public async getAppLanguages(appId: string): Promise<IAppStorageItem['languageContent']> {
const { languages } = await APIClient.get(`/v1/apps/${appId}/languages`);
const { languages } = await APIClient.get(`/apps/${appId}/languages`);
return languages;
}
public async installApp(appId: string, version: string, permissionsGranted: IPermission[]): Promise<IDeletedInstalledApp> {
const { app } = await APIClient.post('/v1/apps/', {
public async installApp(appId: string, version: string, permissionsGranted: IPermission[]): Promise<App> {
const { app } = await APIClient.post('/apps', {
appId,
marketplace: true,
version,
@ -214,48 +221,48 @@ class AppClientOrchestrator {
}
public async updateApp(appId: string, version: string, permissionsGranted: IPermission[]): Promise<App> {
const { app } = await APIClient.post(`/v1/apps/${appId}`, {
const result = (await (APIClient.post as any)(`/apps/${appId}` as any, {
appId,
marketplace: true,
version,
permissionsGranted,
});
return app;
}
})) as any;
public uninstallApp(appId: string): IDeletedInstalledApp {
return APIClient.delete(`apps/${appId}`);
}
public syncApp(appId: string): IAppSynced {
return APIClient.post(`/v1/apps/${appId}/sync`);
if ('app' in result) {
return result;
}
throw new Error('App not found');
}
public async setAppStatus(appId: string, status: AppStatus): Promise<string> {
const { status: effectiveStatus } = await APIClient.post(`/v1/apps/${appId}/status`, { status });
const { status: effectiveStatus } = await APIClient.post(`/apps/${appId}/status`, { status });
return effectiveStatus;
}
public enableApp(appId: string): Promise<string> {
return this.setAppStatus(appId, AppStatus.MANUALLY_ENABLED);
}
public disableApp(appId: string): Promise<string> {
return this.setAppStatus(appId, AppStatus.MANUALLY_ENABLED);
}
public buildExternalUrl(appId: string, purchaseType = 'buy', details = false): IAppExternalURL {
return APIClient.get('/v1/apps', {
public async buildExternalUrl(appId: string, purchaseType: 'buy' | 'subscription' = 'buy', details = false): Promise<IAppExternalURL> {
const result = await APIClient.get('/apps', {
buildExternalUrl: 'true',
appId,
purchaseType,
details,
details: `${details}`,
});
if ('url' in result) {
return result;
}
throw new Error('Failed to build external url');
}
public async getCategories(): Promise<ICategory[]> {
const categories = await APIClient.get('/v1/apps', { categories: 'true' });
return categories;
public async getCategories(): Promise<Serialized<ICategory>[]> {
const result = await APIClient.get('/apps', { categories: 'true' });
if ('categories' in result) {
return result.categories;
}
throw new Error('Categories not found');
}
public getUIHost(): RealAppsEngineUIHost {
@ -267,7 +274,7 @@ export const Apps = new AppClientOrchestrator();
Meteor.startup(() => {
CachedCollectionManager.onLogin(() => {
Meteor.call('apps/is-enabled', (error: Error, isEnabled: boolean) => {
Meteor.call('/apps/is-enabled', (error: Error, isEnabled: boolean) => {
if (error) {
Apps.handleError(error);
return;
@ -279,7 +286,7 @@ Meteor.startup(() => {
});
Tracker.autorun(() => {
const isEnabled = settings.get('Apps_Framework_enabled');
const isEnabled = settings.get('/Apps_Framework_enabled');
Apps.load(isEnabled);
});
});

@ -49,15 +49,13 @@ function wrapMeteorDDPCalls(): void {
});
Meteor.connection.onMessage(_message);
};
const method = encodeURIComponent(message.method.replace(/\//g, ':'));
APIClient.post(
`/v1/${endpoint}/${encodeURIComponent(message.method.replace(/\//g, ':'))}` as Parameters<typeof APIClient.post>[0],
restParams as any,
)
APIClient.post(`/v1/${endpoint}/${method}`, restParams)
.then(({ message: _message }) => {
processResult(_message);
if (message.method === 'login') {
const parsedMessage = DDPCommon.parseDDP(_message) as { result?: { token?: string } };
const parsedMessage = DDPCommon.parseDDP(_message as any) as { result?: { token?: string } };
if (parsedMessage.result?.token) {
Meteor.loginWithToken(parsedMessage.result.token);
}

@ -81,10 +81,35 @@ export const synchronizeUserData = async (uid: Meteor.User['_id']): Promise<RawU
}
});
const userData = await APIClient.get('/v1/me');
const { ldap, lastLogin, services, ...userData } = await APIClient.get('/v1/me');
// email?: {
// verificationTokens?: IUserEmailVerificationToken[];
// };
// export interface IUserEmailVerificationToken {
// token: string;
// address: string;
// when: Date;
// }
if (userData) {
updateUser({
...userData,
...(services && {
...services,
...(services.email?.verificationTokens && {
email: {
verificationTokens: services.email.verificationTokens.map((token) => ({
...token,
when: new Date(token.when),
})),
},
}),
}),
...(lastLogin && {
lastLogin: new Date(lastLogin),
}),
ldap: Boolean(ldap),
createdAt: new Date(userData.createdAt),
_updatedAt: new Date(userData._updatedAt),
});

@ -30,20 +30,20 @@ const callEndpoint = <TMethod extends Method, TPath extends PathFor<TMethod>>(
): Promise<Serialized<OperationResult<TMethod, MatchPathPattern<TPath>>>> => {
switch (method) {
case 'GET':
return APIClient.get(path as Parameters<typeof APIClient.get>[0], params) as any;
return APIClient.get(path as Parameters<typeof APIClient.get>[0], params as any | undefined) as any;
case 'POST':
return APIClient.post(path as Parameters<typeof APIClient.post>[0], params) as ReturnType<typeof APIClient.post>;
return APIClient.post(path as Parameters<typeof APIClient.post>[0], params as never) as ReturnType<typeof APIClient.post>;
case 'DELETE':
return APIClient.delete(path as Parameters<typeof APIClient.delete>[0], params) as ReturnType<typeof APIClient.delete>;
return APIClient.delete(path as Parameters<typeof APIClient.delete>[0], params as never) as ReturnType<typeof APIClient.delete>;
default:
throw new Error('Invalid HTTP method');
}
};
const uploadToEndpoint = (endpoint: PathFor<'POST'>, formData: any): Promise<UploadResult> => APIClient.post(endpoint, formData);
const uploadToEndpoint = (endpoint: PathFor<'POST'>, formData: any): Promise<UploadResult> => APIClient.post(endpoint, formData as never);
const getStream = (streamName: string, options: {} = {}): (<T>(eventName: string, callback: (data: T) => void) => () => void) => {
const streamer = Meteor.StreamerCentral.instances[streamName]

@ -4,7 +4,7 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useTranslation, useCurrentRoute, useRoute, useRouteParameter } from '@rocket.chat/ui-contexts';
import React, { useState, useCallback, useRef, FC } from 'react';
import { ISettings, ISettingsPayload } from '../../../../app/apps/client/@types/IOrchestrator';
import { ISettings } from '../../../../app/apps/client/@types/IOrchestrator';
import { Apps } from '../../../../app/apps/client/orchestrator';
import Page from '../../../components/Page';
import APIsDisplay from './APIsDisplay';
@ -49,7 +49,7 @@ const AppDetailsPage: FC<{ id: string }> = function AppDetailsPage({ id }) {
(Object.values(settings || {}) as ISetting[]).map((value) => ({
...value,
value: current?.[value.id],
})) as unknown as ISettingsPayload,
})),
);
} catch (e) {
handleAPIError(e);

@ -1,7 +1,6 @@
import { useTranslation } from '@rocket.chat/ui-contexts';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { ICategory } from '../../../../../app/apps/client/@types/IOrchestrator';
import { Apps } from '../../../../../app/apps/client/orchestrator';
import { CategoryDropdownItem, CategoryDropDownListProps } from '../definitions/CategoryDropdownDefinitions';
import { handleAPIError } from '../helpers';
@ -21,7 +20,7 @@ export const useCategories = (): [
try {
const fetchedCategories = await Apps.getCategories();
const mappedCategories = fetchedCategories.map((currentCategory: ICategory) => ({
const mappedCategories = fetchedCategories.map((currentCategory) => ({
id: currentCategory.id,
label: currentCategory.title,
checked: false,

@ -116,6 +116,7 @@ export interface IUser extends IRocketChatRecord {
status?: UserStatus;
statusConnection?: string;
lastLogin?: Date;
bio?: string;
avatarOrigin?: string;
avatarETag?: string;
utcOffset?: number;

@ -1,13 +1,26 @@
import type { IApiEndpointMetadata } from '@rocket.chat/apps-engine/definition/api';
import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus';
import type { IExternalComponent } from '@rocket.chat/apps-engine/definition/externalComponent';
import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission';
import type { ISetting } from '@rocket.chat/apps-engine/definition/settings';
import type { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui';
import type { ISetting, AppScreenshot, App } from '@rocket.chat/core-typings';
import type { AppScreenshot, App } from '@rocket.chat/core-typings';
export type AppsEndpoints = {
'/apps/externalComponents': {
GET: () => { externalComponents: IExternalComponent[] };
};
'/apps/:id': {
GET: (params: { marketplace?: 'true' | 'false'; version?: string; appVersion?: string; update?: 'true' | 'false' }) => {
app: App;
};
DELETE: () => void;
POST: (params: { marketplace: boolean; version: string; permissionsGranted: IPermission[]; appId: string }) => {
app: App;
};
};
'/apps/actionButtons': {
GET: () => IUIActionButton[];
};
@ -18,8 +31,9 @@ export type AppsEndpoints = {
'/apps/:id/settings': {
GET: () => {
[key: string]: ISetting;
settings: { [key: string]: ISetting };
};
POST: (params: { settings: ISetting[] }) => { updated: { [key: string]: ISetting } };
};
'/apps/:id/screenshots': {
@ -34,8 +48,49 @@ export type AppsEndpoints = {
};
};
'/apps/:id': {
GET: (params: { marketplace?: 'true' | 'false'; update?: 'true' | 'false'; appVersion: string }) => {
'/apps/bundles/:id/apps': {
GET: () => {
apps: App[];
};
};
'/apps/:id/sync': {
POST: () => {
app: App;
};
};
'/apps/:id/status': {
POST: (params: { status: AppStatus }) => {
status: string;
};
};
'/apps': {
GET:
| ((params: { buildExternalUrl: 'true'; purchaseType?: 'buy' | 'subscription'; appId?: string; details?: 'true' | 'false' }) => {
url: string;
})
| ((params: {
purchaseType?: 'buy' | 'subscription';
marketplace?: 'true' | 'false';
version?: string;
appId?: string;
details?: 'true' | 'false';
}) => {
apps: App[];
})
| ((params: { categories: 'true' | 'false' }) => {
categories: {
createdDate: string;
description: string;
id: string;
modifiedDate: Date;
title: string;
}[];
});
POST: (params: { appId: string; marketplace: boolean; version: string; permissionsGranted: IPermission[] }) => {
app: App;
};
};

@ -1,35 +1,48 @@
import type { IUser, Serialized } from '@rocket.chat/core-typings';
import type { IUser } from '@rocket.chat/core-typings';
type RawUserData = Serialized<
Pick<
IUser,
| '_id'
| 'type'
| 'name'
| 'username'
| 'emails'
| 'status'
| 'statusDefault'
| 'statusText'
| 'statusConnection'
| 'avatarOrigin'
| 'utcOffset'
| 'language'
| 'settings'
| 'roles'
| 'active'
| 'defaultRoom'
| 'customFields'
| 'statusLivechat'
| 'oauth'
| 'createdAt'
| '_updatedAt'
| 'avatarETag'
>
>;
type Keys =
| 'name'
| 'username'
| 'nickname'
| 'emails'
| 'status'
| 'statusDefault'
| 'statusText'
| 'statusConnection'
| 'bio'
| 'avatarOrigin'
| 'utcOffset'
| 'language'
| 'settings'
| 'idleTimeLimit'
| 'roles'
| 'active'
| 'defaultRoom'
| 'customFields'
| 'requirePasswordChange'
| 'requirePasswordChangeReason'
| 'services.github'
| 'services.gitlab'
| 'services.tokenpass'
| 'services.password.bcrypt'
| 'services.totp.enabled'
| 'services.email2fa.enabled'
| 'statusLivechat'
| 'banners'
| 'oauth.authorizedClients'
| '_updatedAt'
| 'avatarETag'
| 'extension';
export type MeEndpoints = {
'/v1/me': {
GET: () => RawUserData;
GET: (params: { fields: Record<Keys, 0> | Record<Keys, 1>; user: IUser }) => IUser & {
email?: string;
settings: {
profile: {};
preferences: unknown;
};
avatarUrl: string;
};
};
};

@ -180,45 +180,35 @@ export type MiscEndpoints = {
}[];
};
};
'me': {
GET: (params: { fields: { [k: string]: number }; user: IUser }) => IUser & {
email?: string;
settings: {
profile: {};
preferences: unknown;
};
avatarUrl: string;
};
};
'shield.svg': {
'/v1/shield.svg': {
GET: (params: ShieldSvg) => {
svg: string;
};
};
'spotlight': {
'/v1/spotlight': {
GET: (params: Spotlight) => {
users: Pick<IUser, 'username' | 'name' | 'status' | 'statusText' | 'avatarETag'>[];
rooms: IRoom[];
};
};
'directory': {
'/v1/directory': {
GET: (params: Directory) => PaginatedResult<{
result: (IUser | IRoom | ITeam)[];
}>;
};
'method.call': {
POST: (params: MethodCall) => {
result: unknown;
'/v1/method.call/:method': {
POST: (params: { message: string }) => {
message: unknown;
};
};
'method.callAnon': {
POST: (params: MethodCallAnon) => {
result: unknown;
'/v1/method.callAnon/:method': {
POST: (params: { message: string }) => {
message: unknown;
};
};
};

Loading…
Cancel
Save