diff --git a/.github/auto-label-action-config.json b/.github/auto-label-action-config.json new file mode 100644 index 0000000000000..0967ef424bce6 --- /dev/null +++ b/.github/auto-label-action-config.json @@ -0,0 +1 @@ +{} diff --git a/.github/workflows/auto-label.yml b/.github/workflows/auto-label.yml new file mode 100644 index 0000000000000..2a453ccdb4d25 --- /dev/null +++ b/.github/workflows/auto-label.yml @@ -0,0 +1,11 @@ +name: 'Auto label QA' +on: + pull_request: + types: [opened, synchronize, labeled, unlabeled] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: ggazzo/gh-action-auto-label@beta-5 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.kodiak.toml b/.kodiak.toml index bbbfca713a5ea..3865c8954586f 100644 --- a/.kodiak.toml +++ b/.kodiak.toml @@ -1,10 +1,13 @@ # .kodiak.toml version = 1 - [merge] method = "squash" -automerge_label = ["stat: ready to merge", "QA tested", "automerge"] +automerge_label = ["stat: ready to merge", "automerge"] +block_on_neutral_required_check_runs = true +blocking_labels = ["stat: needs QA", "Invalid PR Title"] +prioritize_ready_to_merge = true + [merge.message] title = "pull_request_title" # default: "github_default" diff --git a/apps/meteor/.docker/Dockerfile.rhel b/apps/meteor/.docker/Dockerfile.rhel index 7ee11ce244155..f4591110f5300 100644 --- a/apps/meteor/.docker/Dockerfile.rhel +++ b/apps/meteor/.docker/Dockerfile.rhel @@ -1,6 +1,6 @@ FROM registry.access.redhat.com/ubi8/nodejs-12 -ENV RC_VERSION 5.0.0 +ENV RC_VERSION 5.0.0-develop MAINTAINER buildmaster@rocket.chat diff --git a/apps/meteor/.mocharc.api.js b/apps/meteor/.mocharc.api.js index 934fd1509ae15..c7994105d68a2 100644 --- a/apps/meteor/.mocharc.api.js +++ b/apps/meteor/.mocharc.api.js @@ -9,5 +9,10 @@ module.exports = { timeout: 10000, bail: true, file: 'tests/end-to-end/teardown.js', - spec: ['tests/unit/app/api/server/v1/*.spec.ts', 'tests/end-to-end/api/*.js', 'tests/end-to-end/api/*.ts', 'tests/end-to-end/apps/*.js'], + spec: [ + 'tests/unit/app/api/server/v1/**/*.spec.ts', + 'tests/end-to-end/api/*.js', + 'tests/end-to-end/api/*.ts', + 'tests/end-to-end/apps/*.js', + ], }; diff --git a/apps/meteor/.storybook/main.js b/apps/meteor/.storybook/main.js index afb264d668512..71d317a497ca1 100644 --- a/apps/meteor/.storybook/main.js +++ b/apps/meteor/.storybook/main.js @@ -3,8 +3,24 @@ const { resolve, relative, join } = require('path'); const webpack = require('webpack'); module.exports = { - stories: ['../app/**/*.stories.{js,tsx}', '../client/**/*.stories.{js,tsx}', '../ee/**/*.stories.{js,tsx}'], - addons: ['@storybook/addon-essentials', '@storybook/addon-interactions', '@storybook/addon-postcss'], + stories: [ + '../client/**/*.stories.{js,tsx}', + '../app/**/*.stories.{js,tsx}', + '../ee/app/**/*.stories.{js,tsx}', + '../ee/client/**/*.stories.{js,tsx}', + ], + addons: [ + '@storybook/addon-essentials', + '@storybook/addon-interactions', + { + name: '@storybook/addon-postcss', + options: { + postcssLoaderOptions: { + implementation: require('postcss'), + }, + }, + }, + ], webpackFinal: async (config) => { const cssRule = config.module.rules.find(({ test }) => test.test('index.css')); diff --git a/apps/meteor/app/api/server/api.d.ts b/apps/meteor/app/api/server/api.d.ts index e8a8634e06154..ebc4871610e33 100644 --- a/apps/meteor/app/api/server/api.d.ts +++ b/apps/meteor/app/api/server/api.d.ts @@ -9,6 +9,7 @@ import type { } from '@rocket.chat/rest-typings'; import type { IUser, IMethodConnection, IRoom } from '@rocket.chat/core-typings'; import type { ValidateFunction } from 'ajv'; +import type { Request, Response } from 'express'; import { ITwoFactorOptions } from '../../2fa/server/code'; @@ -70,20 +71,15 @@ type Options = ( validateParams?: ValidateFunction; }; -type Request = { - method: 'GET' | 'POST' | 'PUT' | 'DELETE'; - url: string; - headers: Record; - body: any; -}; - type PartialThis = { readonly request: Request & { query: Record }; + readonly response: Response; }; type ActionThis = { readonly requestIp: string; urlParams: UrlParams; + readonly response: Response; // TODO make it unsafe readonly queryParams: TMethod extends 'GET' ? TOptions extends { validateParams: ValidateFunction } @@ -97,6 +93,9 @@ type ActionThis>; readonly request: Request; + + readonly queryOperations: TOptions extends { queryOperations: infer T } ? T : never; + /* @deprecated */ requestParams(): OperationParams; getLoggedInUser(): TOptions extends { authRequired: true } ? IUser : IUser | undefined; @@ -112,6 +111,8 @@ type ActionThis declare class APIClass { fieldSeparator: string; + updateRateLimiterDictionaryForRoute(route: string, rateLimiterDictionary: number): void; + limitedUserFieldsToExclude(fields: { [x: string]: unknown }, limitedUserFieldsToExclude: unknown): { [x: string]: unknown }; limitedUserFieldsToExcludeIfIsPrivilegedUser( diff --git a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts index 7e3592e765a0a..4710aac5bc256 100644 --- a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts +++ b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts @@ -70,7 +70,7 @@ API.helperMethods.set( if (typeof fields === 'object') { let nonSelectableFields = Object.keys(API.v1.defaultFieldsToExclude); if (this.request.route.includes('/v1/users.')) { - const getFields = () => + const getFields = (): string[] => Object.keys( hasPermission(this.userId, 'view-full-other-user-info') ? API.v1.limitedUserFieldsToExcludeIfIsPrivilegedUser diff --git a/apps/meteor/app/api/server/lib/emoji-custom.js b/apps/meteor/app/api/server/lib/emoji-custom.js deleted file mode 100644 index 1d7dde2706646..0000000000000 --- a/apps/meteor/app/api/server/lib/emoji-custom.js +++ /dev/null @@ -1,20 +0,0 @@ -import { EmojiCustom } from '../../../models/server/raw'; - -export async function findEmojisCustom({ query = {}, pagination: { offset, count, sort } }) { - const cursor = EmojiCustom.find(query, { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - }); - - const total = await cursor.count(); - - const emojis = await cursor.toArray(); - - return { - emojis, - count: emojis.length, - offset, - total, - }; -} diff --git a/apps/meteor/app/api/server/lib/emoji-custom.ts b/apps/meteor/app/api/server/lib/emoji-custom.ts new file mode 100644 index 0000000000000..2c3f660c67f15 --- /dev/null +++ b/apps/meteor/app/api/server/lib/emoji-custom.ts @@ -0,0 +1,34 @@ +import { IEmojiCustom, ILivechatDepartmentRecord } from '@rocket.chat/core-typings'; +import { FilterQuery, SortOptionObject } from 'mongodb'; + +import { EmojiCustom } from '../../../models/server/raw'; + +export async function findEmojisCustom({ + query = {}, + pagination: { offset, count, sort }, +}: { + query: FilterQuery; + pagination: { offset: number; count: number; sort: SortOptionObject }; +}): Promise<{ + emojis: IEmojiCustom[]; + count: number; + offset: any; + total: number; +}> { + const cursor = EmojiCustom.find(query, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + }); + + const total = await cursor.count(); + + const emojis = await cursor.toArray(); + + return { + emojis, + count: emojis.length, + offset, + total, + }; +} diff --git a/apps/meteor/app/api/server/lib/getUploadFormData.ts b/apps/meteor/app/api/server/lib/getUploadFormData.ts index c3ce2652ca1ac..c9cf0263c5d84 100644 --- a/apps/meteor/app/api/server/lib/getUploadFormData.ts +++ b/apps/meteor/app/api/server/lib/getUploadFormData.ts @@ -1,49 +1,90 @@ +import { Readable } from 'stream'; + +import { Meteor } from 'meteor/meteor'; +import type { Request } from 'express'; import busboy from 'busboy'; -import { Request } from 'express'; +import { ValidateFunction } from 'ajv'; -export interface IFormDataFields { - file: any; +type UploadResult = { + file: Readable; filename: string; encoding: string; mimetype: string; fileBuffer: Buffer; -} - -export interface IFormDataUpload { - [key: string]: IFormDataFields | any; -} +}; -export const getUploadFormData = async ({ request }: { request: Request }): Promise => - new Promise((resolve, reject) => { +export const getUploadFormData = async >( + { request }: { request: Request }, + options: { + field?: T; + validate?: V; + } = {}, +): Promise< + [ + UploadResult, + K extends unknown + ? { + [k: string]: string; + } + : K, + T, + ] +> => + new Promise((resolve, reject) => { const bb = busboy({ headers: request.headers, defParamCharset: 'utf8' }); + const fields: { [K: string]: string } = Object.create(null); - const fields: IFormDataUpload = {}; + let uploadedFile: UploadResult | undefined; - bb.on('file', (fieldname: string, file: any, { filename, encoding, mimeType: mimetype }: Record) => { - const fileData: any[] = []; + let assetName: T | undefined; - file.on('data', (data: any) => fileData.push(data)); + bb.on( + 'file', + ( + fieldname: string, + file: Readable, + { filename, encoding, mimeType: mimetype }: { filename: string; encoding: string; mimeType: string }, + ) => { + const fileData: Uint8Array[] = []; - file.on('end', () => { - if (fields.hasOwnProperty(fieldname)) { - return reject('Just 1 file is allowed'); - } + file.on('data', (data: any) => fileData.push(data)); - fields[fieldname] = { - file, - filename, - encoding, - mimetype, - fileBuffer: Buffer.concat(fileData), - }; - }); - }); + file.on('end', () => { + if (uploadedFile) { + return reject('Just 1 file is allowed'); + } + if (options.field && fieldname !== options.field) { + return reject(new Meteor.Error('invalid-field')); + } + uploadedFile = { + file, + filename, + encoding, + mimetype, + fileBuffer: Buffer.concat(fileData), + }; - bb.on('field', (fieldname: string, value: any) => { + assetName = fieldname as T; + }); + }, + ); + + bb.on('field', (fieldname, value) => { fields[fieldname] = value; }); - bb.on('finish', () => resolve(fields)); + bb.on('finish', () => { + if (!uploadedFile || !assetName) { + return reject('No file uploaded'); + } + if (options.validate === undefined) { + return resolve([uploadedFile, fields, assetName]); + } + if (!options.validate(fields)) { + return reject(`Invalid fields${options.validate.errors?.join(', ')}`); + } + return resolve([uploadedFile, fields, assetName]); + }); request.pipe(bb); }); diff --git a/apps/meteor/app/api/server/lib/messages.js b/apps/meteor/app/api/server/lib/messages.ts similarity index 66% rename from apps/meteor/app/api/server/lib/messages.js rename to apps/meteor/app/api/server/lib/messages.ts index d06eef7f06711..194bdabdd2740 100644 --- a/apps/meteor/app/api/server/lib/messages.js +++ b/apps/meteor/app/api/server/lib/messages.ts @@ -1,13 +1,28 @@ +import { IMessage, IUser } from '@rocket.chat/core-typings'; + import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; import { Rooms, Messages, Users } from '../../../models/server/raw'; import { getValue } from '../../../settings/server/raw'; -export async function findMentionedMessages({ uid, roomId, pagination: { offset, count, sort } }) { +export async function findMentionedMessages({ + uid, + roomId, + pagination: { offset, count, sort }, +}: { + uid: string; + roomId: string; + pagination: { offset: number; count: number; sort: [string, number][] }; +}): Promise<{ + messages: IMessage[]; + count: number; + offset: number; + total: number; +}> { const room = await Rooms.findOneById(roomId); if (!(await canAccessRoomAsync(room, { _id: uid }))) { throw new Error('error-not-allowed'); } - const user = await Users.findOneById(uid, { fields: { username: 1 } }); + const user: IUser | null = await Users.findOneById(uid, { fields: { username: 1 } }); if (!user) { throw new Error('invalid-user'); } @@ -30,7 +45,20 @@ export async function findMentionedMessages({ uid, roomId, pagination: { offset, }; } -export async function findStarredMessages({ uid, roomId, pagination: { offset, count, sort } }) { +export async function findStarredMessages({ + uid, + roomId, + pagination: { offset, count, sort }, +}: { + uid: string; + roomId: string; + pagination: { offset: number; count: number; sort: [string, number][] }; +}): Promise<{ + messages: IMessage[]; + count: number; + offset: any; + total: number; +}> { const room = await Rooms.findOneById(roomId); if (!(await canAccessRoomAsync(room, { _id: uid }))) { throw new Error('error-not-allowed'); @@ -58,7 +86,7 @@ export async function findStarredMessages({ uid, roomId, pagination: { offset, c }; } -export async function findSnippetedMessageById({ uid, messageId }) { +export async function findSnippetedMessageById({ uid, messageId }: { uid: string; messageId: string }): Promise { if (!(await getValue('Message_AllowSnippeting'))) { throw new Error('error-not-allowed'); } @@ -83,12 +111,23 @@ export async function findSnippetedMessageById({ uid, messageId }) { throw new Error('error-not-allowed'); } - return { - message: snippet, - }; + return snippet; } -export async function findSnippetedMessages({ uid, roomId, pagination: { offset, count, sort } }) { +export async function findSnippetedMessages({ + uid, + roomId, + pagination: { offset, count, sort }, +}: { + uid: string; + roomId: string; + pagination: { offset: number; count: number; sort: [string, number][] }; +}): Promise<{ + messages: IMessage[]; + count: number; + offset: number; + total: number; +}> { if (!(await getValue('Message_AllowSnippeting'))) { throw new Error('error-not-allowed'); } @@ -116,7 +155,22 @@ export async function findSnippetedMessages({ uid, roomId, pagination: { offset, }; } -export async function findDiscussionsFromRoom({ uid, roomId, text, pagination: { offset, count, sort } }) { +export async function findDiscussionsFromRoom({ + uid, + roomId, + text, + pagination: { offset, count, sort }, +}: { + uid: string; + roomId: string; + text: string; + pagination: { offset: number; count: number; sort: [string, number][] }; +}): Promise<{ + messages: IMessage[]; + count: number; + offset: number; + total: number; +}> { const room = await Rooms.findOneById(roomId); if (!(await canAccessRoomAsync(room, { _id: uid }))) { diff --git a/apps/meteor/app/api/server/lib/rooms.js b/apps/meteor/app/api/server/lib/rooms.ts similarity index 69% rename from apps/meteor/app/api/server/lib/rooms.js rename to apps/meteor/app/api/server/lib/rooms.ts index 0e402dec3ffee..5ffd4798bb786 100644 --- a/apps/meteor/app/api/server/lib/rooms.js +++ b/apps/meteor/app/api/server/lib/rooms.ts @@ -1,15 +1,32 @@ +import { IRoom, ISubscription } from '@rocket.chat/core-typings'; + import { hasPermissionAsync, hasAtLeastOnePermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { Rooms } from '../../../models/server/raw'; import { Subscriptions } from '../../../models/server'; import { adminFields } from '../../../../lib/rooms/adminFields'; -export async function findAdminRooms({ uid, filter, types = [], pagination: { offset, count, sort } }) { +export async function findAdminRooms({ + uid, + filter, + types = [], + pagination: { offset, count, sort }, +}: { + uid: string; + filter: string; + types: string[]; + pagination: { offset: number; count: number; sort: [string, number][] }; +}): Promise<{ + rooms: IRoom[]; + count: number; + offset: number; + total: number; +}> { if (!(await hasPermissionAsync(uid, 'view-room-administration'))) { throw new Error('error-not-authorized'); } - const name = filter && filter.trim(); - const discussion = types && types.includes('discussions'); - const includeTeams = types && types.includes('teams'); + const name = filter?.trim(); + const discussion = types?.includes('discussions'); + const includeTeams = types?.includes('teams'); const showOnlyTeams = types.length === 1 && types.includes('teams'); const typesToRemove = ['discussions', 'teams']; const showTypes = Array.isArray(types) ? types.filter((type) => !typesToRemove.includes(type)) : []; @@ -41,7 +58,7 @@ export async function findAdminRooms({ uid, filter, types = [], pagination: { of }; } -export async function findAdminRoom({ uid, rid }) { +export async function findAdminRoom({ uid, rid }: { uid: string; rid: string }): Promise { if (!(await hasPermissionAsync(uid, 'view-room-administration'))) { throw new Error('error-not-authorized'); } @@ -49,7 +66,9 @@ export async function findAdminRoom({ uid, rid }) { return Rooms.findOneById(rid, { fields: adminFields }); } -export async function findChannelAndPrivateAutocomplete({ uid, selector }) { +export async function findChannelAndPrivateAutocomplete({ uid, selector }: { uid: string; selector: { name: string } }): Promise<{ + items: IRoom[]; +}> { const options = { fields: { _id: 1, @@ -66,7 +85,7 @@ export async function findChannelAndPrivateAutocomplete({ uid, selector }) { const userRoomsIds = Subscriptions.cachedFindByUserId(uid, { fields: { rid: 1 } }) .fetch() - .map((item) => item.rid); + .map((item: Pick) => item.rid); const rooms = await Rooms.findRoomsWithoutDiscussionsByRoomIds(selector.name, userRoomsIds, options).toArray(); @@ -75,7 +94,9 @@ export async function findChannelAndPrivateAutocomplete({ uid, selector }) { }; } -export async function findAdminRoomsAutocomplete({ uid, selector }) { +export async function findAdminRoomsAutocomplete({ uid, selector }: { uid: string; selector: { name: string } }): Promise<{ + items: IRoom[]; +}> { if (!(await hasAtLeastOnePermissionAsync(uid, ['view-room-administration', 'can-audit']))) { throw new Error('error-not-authorized'); } @@ -100,10 +121,21 @@ export async function findAdminRoomsAutocomplete({ uid, selector }) { }; } -export async function findChannelAndPrivateAutocompleteWithPagination({ uid, selector, pagination: { offset, count, sort } }) { +export async function findChannelAndPrivateAutocompleteWithPagination({ + uid, + selector, + pagination: { offset, count, sort }, +}: { + uid: string; + selector: { name: string }; + pagination: { offset: number; count: number; sort: [string, number][] }; +}): Promise<{ + items: IRoom[]; + total: number; +}> { const userRoomsIds = Subscriptions.cachedFindByUserId(uid, { fields: { rid: 1 } }) .fetch() - .map((item) => item.rid); + .map((item: Pick) => item.rid); const options = { fields: { @@ -129,7 +161,9 @@ export async function findChannelAndPrivateAutocompleteWithPagination({ uid, sel }; } -export async function findRoomsAvailableForTeams({ uid, name }) { +export async function findRoomsAvailableForTeams({ uid, name }: { uid: string; name: string }): Promise<{ + items: IRoom[]; +}> { const options = { fields: { _id: 1, @@ -144,9 +178,9 @@ export async function findRoomsAvailableForTeams({ uid, name }) { }, }; - const userRooms = Subscriptions.findByUserIdAndRoles(uid, ['owner'], { fields: { rid: 1 } }) - .fetch() - .map((item) => item.rid); + const userRooms = ( + Subscriptions.findByUserIdAndRoles(uid, ['owner'], { fields: { rid: 1 } }).fetch() as Pick[] + ).map((item) => item.rid); const rooms = await Rooms.findChannelAndGroupListWithoutTeamsByNameStartingByOwner(uid, name, userRooms, options).toArray(); diff --git a/apps/meteor/app/api/server/lib/users.js b/apps/meteor/app/api/server/lib/users.ts similarity index 66% rename from apps/meteor/app/api/server/lib/users.js rename to apps/meteor/app/api/server/lib/users.ts index 1e7eb9a7a2428..7762b9b20a183 100644 --- a/apps/meteor/app/api/server/lib/users.js +++ b/apps/meteor/app/api/server/lib/users.ts @@ -1,9 +1,23 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { ILivechatDepartmentRecord, IUser } from '@rocket.chat/core-typings'; +import { FilterQuery } from 'mongodb'; import { Users } from '../../../models/server/raw'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; -export async function findUsersToAutocomplete({ uid, selector }) { +export async function findUsersToAutocomplete({ + uid, + selector, +}: { + uid: string; + selector: { + exceptions: string[]; + conditions: FilterQuery; + term: string; + }; +}): Promise<{ + items: Required>[]; +}> { if (!(await hasPermissionAsync(uid, 'view-outside-room'))) { return { items: [] }; } @@ -39,8 +53,8 @@ export async function findUsersToAutocomplete({ uid, selector }) { * Returns a new query object with the inclusive fields only * @param {Object} query search query for matching rows */ -export function getInclusiveFields(query) { - const newQuery = {}; +export function getInclusiveFields(query: { [k: string]: 1 }): {} { + const newQuery = Object.create(null); for (const [key, value] of Object.entries(query)) { if (value === 1) { @@ -55,7 +69,7 @@ export function getInclusiveFields(query) { * get the default fields if **fields** are empty (`{}`) or `undefined`/`null` * @param {Object|null|undefined} fields the fields from parsed jsonQuery */ -export function getNonEmptyFields(fields) { +export function getNonEmptyFields(fields: { [k: string]: 1 | 0 }): { [k: string]: 1 } { const defaultFields = { name: 1, username: 1, @@ -65,7 +79,7 @@ export function getNonEmptyFields(fields) { active: 1, avatarETag: 1, lastLogin: 1, - }; + } as const; if (!fields || Object.keys(fields).length === 0) { return defaultFields; @@ -74,11 +88,21 @@ export function getNonEmptyFields(fields) { return { ...defaultFields, ...fields }; } +const _defaultQuery = { + $or: [ + { 'emails.address': { $regex: '', $options: 'i' } }, + { username: { $regex: '', $options: 'i' } }, + { name: { $regex: '', $options: 'i' } }, + ], +}; + /** * get the default query if **query** is empty (`{}`) or `undefined`/`null` * @param {Object|null|undefined} query the query from parsed jsonQuery */ -export function getNonEmptyQuery(query) { + +type Query = { [k: string]: unknown }; +export function getNonEmptyQuery(query: Query): typeof _defaultQuery | (typeof _defaultQuery & Query) { const defaultQuery = { $or: [ { 'emails.address': { $regex: '', $options: 'i' } }, diff --git a/apps/meteor/app/api/server/v1/assets.ts b/apps/meteor/app/api/server/v1/assets.ts index 04dacd04233d9..2f2ffff1aff7a 100644 --- a/apps/meteor/app/api/server/v1/assets.ts +++ b/apps/meteor/app/api/server/v1/assets.ts @@ -1,5 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { Request } from 'express'; + import { isAssetsUnsetAssetProps } from '@rocket.chat/rest-typings'; import { RocketChatAssets } from '../../../assets/server'; @@ -10,21 +11,20 @@ API.v1.addRoute( 'assets.setAsset', { authRequired: true }, { - async post() { - const { refreshAllClients, ...files } = await getUploadFormData({ request: this.request as Request }); + post() { + const [asset, { refreshAllClients }, assetName] = await; + getUploadFormData({ + request: this.request, + }); const assetsKeys = Object.keys(RocketChatAssets.assets); - const [assetName] = Object.keys(files); - const isValidAsset = assetsKeys.includes(assetName); if (!isValidAsset) { throw new Meteor.Error('error-invalid-asset', 'Invalid asset'); } Meteor.runAsUser(this.userId, () => { - const { [assetName]: asset } = files; - Meteor.call('setAsset', asset.fileBuffer, asset.mimetype, assetName); if (refreshAllClients) { Meteor.call('refreshClients'); diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index e1ab4da8e8d93..484c2a0770811 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -129,9 +129,9 @@ API.v1.addRoute( }, { get() { - const { roomId, unreads, oldest, latest, showThreadMessages, inclusive } = this.queryParams; + const { unreads, oldest, latest, showThreadMessages, inclusive, ...params } = this.queryParams; const findResult = findChannelByIdOrName({ - params: { roomId }, + params, checkedArchived: false, }); @@ -184,13 +184,13 @@ API.v1.addRoute( }, { post() { - const { roomId, joinCode } = this.bodyParams; - const findResult = findChannelByIdOrName({ params: { roomId } }); + const { joinCode, ...params } = this.bodyParams; + const findResult = findChannelByIdOrName({ params }); Meteor.call('joinRoom', findResult._id, joinCode); return API.v1.success({ - channel: findChannelByIdOrName({ params: { roomId }, userId: this.userId }), + channel: findChannelByIdOrName({ params, userId: this.userId }), }); }, }, @@ -204,15 +204,15 @@ API.v1.addRoute( }, { post() { - const { roomId /* userId */ } = this.bodyParams; - const findResult = findChannelByIdOrName({ params: { roomId } }); + const { ...params /* userId */ } = this.bodyParams; + const findResult = findChannelByIdOrName({ params }); const user = this.getUserFromParams(); Meteor.call('removeUserFromRoom', { rid: findResult._id, username: user.username }); return API.v1.success({ - channel: findChannelByIdOrName({ params: { roomId }, userId: this.userId }), + channel: findChannelByIdOrName({ params, userId: this.userId }), }); }, }, @@ -226,15 +226,15 @@ API.v1.addRoute( }, { post() { - const { roomId } = this.bodyParams; - const findResult = findChannelByIdOrName({ params: { roomId } }); + const { ...params } = this.bodyParams; + const findResult = findChannelByIdOrName({ params }); Meteor.runAsUser(this.userId, () => { Meteor.call('leaveRoom', findResult._id); }); return API.v1.success({ - channel: findChannelByIdOrName({ params: { roomId }, userId: this.userId }), + channel: findChannelByIdOrName({ params, userId: this.userId }), }); }, }, @@ -297,10 +297,10 @@ API.v1.addRoute( }, { post() { - const { roomId } = this.bodyParams; + const { ...params } = this.bodyParams; const findResult = findChannelByIdOrName({ - params: { roomId }, + params, checkedArchived: false, }); @@ -329,9 +329,7 @@ API.v1.addRoute( }, { post() { - const { roomId } = this.bodyParams; - - const findResult = findChannelByIdOrName({ params: { roomId } }); + const findResult = findChannelByIdOrName({ params: this.bodyParams }); if (findResult.ro === this.bodyParams.readOnly) { return API.v1.failure('The channel read only setting is the same as what it would be changed to.'); @@ -340,7 +338,7 @@ API.v1.addRoute( Meteor.call('saveRoomSettings', findResult._id, 'readOnly', this.bodyParams.readOnly); return API.v1.success({ - channel: findChannelByIdOrName({ params: { roomId }, userId: this.userId }), + channel: findChannelByIdOrName({ params: this.bodyParams, userId: this.userId }), }); }, }, @@ -354,9 +352,9 @@ API.v1.addRoute( }, { post() { - const { roomId, announcement } = this.bodyParams; + const { announcement, ...params } = this.bodyParams; - const findResult = findChannelByIdOrName({ params: { roomId } }); + const findResult = findChannelByIdOrName({ params }); Meteor.call('saveRoomSettings', findResult._id, 'roomAnnouncement', announcement); @@ -379,23 +377,19 @@ API.v1.addRoute( const { offset, count } = this.getPaginationItems(); const { sort } = this.parseJsonQuery(); - const mentions = Meteor.runAsUser(this.userId, () => - Meteor.call('getUserMentionsByChannel', { - roomId, - options: { - sort: sort || { ts: 1 }, - skip: offset, - limit: count, - }, - }), - ); - - const allMentions = Meteor.runAsUser(this.userId, () => - Meteor.call('getUserMentionsByChannel', { - roomId, - options: {}, - }), - ); + const mentions = Meteor.call('getUserMentionsByChannel', { + roomId, + options: { + sort: sort || { ts: 1 }, + skip: offset, + limit: count, + }, + }); + + const allMentions = Meteor.call('getUserMentionsByChannel', { + roomId, + options: {}, + }); return API.v1.success({ mentions, @@ -415,9 +409,9 @@ API.v1.addRoute( }, { get() { - const { roomId } = this.queryParams; + const { ...params } = this.queryParams; - const findResult = findChannelByIdOrName({ params: { roomId } }); + const findResult = findChannelByIdOrName({ params }); const moderators = Subscriptions.findByRoomIdAndRoles(findResult._id, ['moderator'], { fields: { u: 1 }, diff --git a/apps/meteor/app/api/server/v1/emoji-custom.ts b/apps/meteor/app/api/server/v1/emoji-custom.ts index 671d26fc1ef2a..bd29a6adc3144 100644 --- a/apps/meteor/app/api/server/v1/emoji-custom.ts +++ b/apps/meteor/app/api/server/v1/emoji-custom.ts @@ -68,13 +68,12 @@ API.v1.addRoute( { authRequired: true }, { async post() { - const { emoji, ...fields } = await getUploadFormData({ - request: this.request as Request, - }); - - if (!emoji) { - throw new Meteor.Error('invalid-field'); - } + const [emoji, fields] = await getUploadFormData( + { + request: this.request, + }, + { field: 'emoji' }, + ); const isUploadable = await Media.isImage(emoji.fileBuffer); if (!isUploadable) { @@ -84,11 +83,16 @@ API.v1.addRoute( const [, extension] = emoji.mimetype.split('/'); fields.extension = extension; - fields.newFile = true; - fields.aliases = fields.aliases || ''; - - Meteor.call('insertOrUpdateEmoji', fields); - Meteor.call('uploadEmojiCustom', emoji.fileBuffer, emoji.mimetype, fields); + Meteor.call('insertOrUpdateEmoji', { + ...fields, + newFile: true, + aliases: fields.aliases || '', + }); + Meteor.call('uploadEmojiCustom', emoji.fileBuffer, emoji.mimetype, { + ...fields, + newFile: true, + aliases: fields.aliases || '', + }); return API.v1.success(); }, @@ -100,9 +104,12 @@ API.v1.addRoute( { authRequired: true }, { async post() { - const { emoji, ...fields } = await getUploadFormData({ - request: this.request as Request, - }); + const [emoji, fields] = await getUploadFormData( + { + request: this.request, + }, + { field: 'emoji' }, + ); if (!fields._id) { throw new Meteor.Error('The required "_id" query param is missing.'); @@ -116,7 +123,7 @@ API.v1.addRoute( fields.previousName = emojiToUpdate.name; fields.previousExtension = emojiToUpdate.extension; fields.aliases = fields.aliases || ''; - fields.newFile = Boolean(emoji?.fileBuffer.length); + const newFile = Boolean(emoji?.fileBuffer.length); if (fields.newFile) { const isUploadable = Promise.await(Media.isImage(emoji.fileBuffer)); @@ -130,9 +137,9 @@ API.v1.addRoute( fields.extension = emojiToUpdate.extension; } - Meteor.call('insertOrUpdateEmoji', fields); + Meteor.call('insertOrUpdateEmoji', { ...fields, newFile }); if (fields.newFile) { - Meteor.call('uploadEmojiCustom', emoji.fileBuffer, emoji.mimetype, fields); + Meteor.call('uploadEmojiCustom', emoji.fileBuffer, emoji.mimetype, { ...fields, newFile }); } return API.v1.success(); }, diff --git a/apps/meteor/app/api/server/v1/misc.ts b/apps/meteor/app/api/server/v1/misc.ts index 841cca8e885ba..38e884bd66008 100644 --- a/apps/meteor/app/api/server/v1/misc.ts +++ b/apps/meteor/app/api/server/v1/misc.ts @@ -104,8 +104,6 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; * type: object * gitlab: * type: object - * tokenpass: - * type: object * password: * type: object * properties: diff --git a/apps/meteor/app/api/server/v1/rooms.js b/apps/meteor/app/api/server/v1/rooms.js index 5d722a6e9fc4e..3efbf9f00116c 100644 --- a/apps/meteor/app/api/server/v1/rooms.js +++ b/apps/meteor/app/api/server/v1/rooms.js @@ -85,10 +85,13 @@ API.v1.addRoute( return API.v1.unauthorized(); } - const { file, ...fields } = Promise.await( - getUploadFormData({ - request: this.request, - }), + const [file, fields] = Promise.await( + getUploadFormData( + { + request: this.request, + }, + { field: 'file' }, + ), ); if (!file) { diff --git a/apps/meteor/app/api/server/v1/users.js b/apps/meteor/app/api/server/v1/users.ts similarity index 65% rename from apps/meteor/app/api/server/v1/users.js rename to apps/meteor/app/api/server/v1/users.ts index d57d477a4cf33..8e6808fd34b42 100644 --- a/apps/meteor/app/api/server/v1/users.js +++ b/apps/meteor/app/api/server/v1/users.ts @@ -1,57 +1,236 @@ +import { + isUserCreateParamsPOST, + isUserSetActiveStatusParamsPOST, + isUserDeactivateIdleParamsPOST, + isUsersInfoParamsGetProps, + isUserRegisterParamsPOST, + isUserLogoutParamsPOST, + isUsersListTeamsProps, + isUsersAutocompleteProps, + isUsersSetAvatarProps, + isUsersUpdateParamsPOST, + isUsersUpdateOwnBasicInfoParamsPOST, + isUsersSetPreferencesParamsPOST, +} from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; import { Match, check } from 'meteor/check'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import _ from 'underscore'; +import { IExportOperation, IPersonalAccessToken, IUser } from '@rocket.chat/core-typings'; import { Users, Subscriptions } from '../../../models/server'; import { Users as UsersRaw } from '../../../models/server/raw'; -import { hasPermission } from '../../../authorization'; +import { hasPermission } from '../../../authorization/server'; import { settings } from '../../../settings/server'; -import { getURL } from '../../../utils'; import { validateCustomFields, saveUser, saveCustomFieldsWithoutValidation, checkUsernameAvailability, + setStatusText, setUserAvatar, saveCustomFields, - setStatusText, } from '../../../lib/server'; import { getFullUserDataByIdOrUsername } from '../../../lib/server/functions/getFullUserData'; import { API } from '../api'; -import { getUploadFormData } from '../lib/getUploadFormData'; import { findUsersToAutocomplete, getInclusiveFields, getNonEmptyFields, getNonEmptyQuery } from '../lib/users'; import { getUserForCheck, emailCheck } from '../../../2fa/server/code'; import { resetUserE2EEncriptionKey } from '../../../../server/lib/resetUserE2EKey'; -import { setUserStatus } from '../../../../imports/users-presence/server/activeUsers'; import { resetTOTP } from '../../../2fa/server/functions/resetTOTP'; import { Team } from '../../../../server/sdk'; import { isValidQuery } from '../lib/isValidQuery'; +import { setUserStatus } from '../../../../imports/users-presence/server/activeUsers'; +import { getURL } from '../../../utils/server'; +import { getUploadFormData } from '../lib/getUploadFormData'; API.v1.addRoute( - 'users.create', - { authRequired: true }, + 'users.getAvatar', + { authRequired: false }, + { + get() { + const user = this.getUserFromParams(); + + const url = getURL(`/avatar/${user.username}`, { cdn: false, full: true }); + this.response.setHeader('Location', url); + + return { + statusCode: 307, + body: url, + }; + }, + }, +); + +API.v1.addRoute( + 'users.update', + { authRequired: true, twoFactorRequired: true, validateParams: isUsersUpdateParamsPOST }, + { + post() { + const userData = { _id: this.bodyParams.userId, ...this.bodyParams.data }; + + Meteor.runAsUser(this.userId, () => saveUser(this.userId, userData)); + + if (this.bodyParams.data.customFields) { + saveCustomFields(this.bodyParams.userId, this.bodyParams.data.customFields); + } + + if (typeof this.bodyParams.data.active !== 'undefined') { + const { + userId, + data: { active }, + confirmRelinquish, + } = this.bodyParams; + + Meteor.call('setUserActiveStatus', userId, active, Boolean(confirmRelinquish)); + } + const { fields } = this.parseJsonQuery(); + + return API.v1.success({ user: Users.findOneById(this.bodyParams.userId, { fields }) }); + }, + }, +); + +API.v1.addRoute( + 'users.updateOwnBasicInfo', + { authRequired: true, validateParams: isUsersUpdateOwnBasicInfoParamsPOST }, + { + post() { + const userData = { + email: this.bodyParams.data.email, + realname: this.bodyParams.data.name, + username: this.bodyParams.data.username, + nickname: this.bodyParams.data.nickname, + statusText: this.bodyParams.data.statusText, + newPassword: this.bodyParams.data.newPassword, + typedPassword: this.bodyParams.data.currentPassword, + }; + + // saveUserProfile now uses the default two factor authentication procedures, so we need to provide that + const twoFactorOptions = !userData.typedPassword + ? null + : { + twoFactorCode: userData.typedPassword, + twoFactorMethod: 'password', + }; + + Meteor.call('saveUserProfile', userData, this.bodyParams.customFields, twoFactorOptions); + + return API.v1.success({ + user: Users.findOneById(this.userId, { fields: API.v1.defaultFieldsToExclude }), + }); + }, + }, +); + +API.v1.addRoute( + 'users.setPreferences', + { authRequired: true, validateParams: isUsersSetPreferencesParamsPOST }, { post() { - check(this.bodyParams, { - email: String, - name: String, - password: String, - username: String, - active: Match.Maybe(Boolean), - bio: Match.Maybe(String), - nickname: Match.Maybe(String), - statusText: Match.Maybe(String), - roles: Match.Maybe(Array), - joinDefaultChannels: Match.Maybe(Boolean), - requirePasswordChange: Match.Maybe(Boolean), - setRandomPassword: Match.Maybe(Boolean), - sendWelcomeEmail: Match.Maybe(Boolean), - verified: Match.Maybe(Boolean), - customFields: Match.Maybe(Object), + if (this.bodyParams.userId && this.bodyParams.userId !== this.userId && !hasPermission(this.userId, 'edit-other-user-info')) { + throw new Meteor.Error('error-action-not-allowed', 'Editing user is not allowed'); + } + const userId = this.bodyParams.userId ? this.bodyParams.userId : this.userId; + if (!Users.findOneById(userId)) { + throw new Meteor.Error('error-invalid-user', 'The optional "userId" param provided does not match any users'); + } + + Meteor.runAsUser(userId, () => Meteor.call('saveUserPreferences', this.bodyParams.data)); + const user = Users.findOneById(userId, { + fields: { + 'settings.preferences': 1, + 'language': 1, + }, + }); + return API.v1.success({ + user: { + _id: user._id, + settings: { + preferences: { + ...user.settings.preferences, + language: user.language, + }, + }, + }, }); + }, + }, +); + +API.v1.addRoute( + 'users.setAvatar', + { authRequired: true, validateParams: isUsersSetAvatarProps }, + { + async post() { + const canEditOtherUserAvatar = hasPermission(this.userId, 'edit-other-user-avatar'); + + if (!settings.get('Accounts_AllowUserAvatarChange') && !canEditOtherUserAvatar) { + throw new Meteor.Error('error-not-allowed', 'Change avatar is not allowed', { + method: 'users.setAvatar', + }); + } + + let user = ((): IUser | undefined => { + if (this.isUserFromParams()) { + return Meteor.users.findOne(this.userId) as IUser | undefined; + } + if (canEditOtherUserAvatar) { + return this.getUserFromParams(); + } + })(); + + if (!user) { + return API.v1.unauthorized(); + } + + if (this.bodyParams.avatarUrl) { + setUserAvatar(user, this.bodyParams.avatarUrl, '', 'url'); + return API.v1.success(); + } + + const [image, fields] = await getUploadFormData( + { + request: this.request, + }, + { + field: 'image', + }, + ); + + if (!image) { + return API.v1.failure("The 'image' param is required"); + } + + const sentTheUserByFormData = fields.userId || fields.username; + if (sentTheUserByFormData) { + if (fields.userId) { + user = Users.findOneById(fields.userId, { fields: { username: 1 } }); + } else if (fields.username) { + user = Users.findOneByUsernameIgnoringCase(fields.username, { fields: { username: 1 } }); + } + + if (!user) { + throw new Meteor.Error('error-invalid-user', 'The optional "userId" or "username" param provided does not match any users'); + } + + const isAnotherUser = this.userId !== user._id; + if (isAnotherUser && !hasPermission(this.userId, 'edit-other-user-avatar')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } + } + + setUserAvatar(user, image.fileBuffer, image.mimetype, 'rest'); + return API.v1.success(); + }, + }, +); + +API.v1.addRoute( + 'users.create', + { authRequired: true, validateParams: isUserCreateParamsPOST }, + { + post() { // New change made by pull request #5152 if (typeof this.bodyParams.joinDefaultChannels === 'undefined') { this.bodyParams.joinDefaultChannels = true; @@ -68,9 +247,7 @@ API.v1.addRoute( } if (typeof this.bodyParams.active !== 'undefined') { - Meteor.runAsUser(this.userId, () => { - Meteor.call('setUserActiveStatus', newUserId, this.bodyParams.active); - }); + Meteor.call('setUserActiveStatus', newUserId, this.bodyParams.active); } const { fields } = this.parseJsonQuery(); @@ -92,9 +269,7 @@ API.v1.addRoute( const user = this.getUserFromParams(); const { confirmRelinquish = false } = this.requestParams(); - Meteor.runAsUser(this.userId, () => { - Meteor.call('deleteUser', user._id, confirmRelinquish); - }); + Meteor.call('deleteUser', user._id, confirmRelinquish); return API.v1.success(); }, @@ -116,52 +291,24 @@ API.v1.addRoute( const { confirmRelinquish = false } = this.requestParams(); - Meteor.runAsUser(this.userId, () => { - Meteor.call('deleteUserOwnAccount', password, confirmRelinquish); - }); + Meteor.call('deleteUserOwnAccount', password, confirmRelinquish); return API.v1.success(); }, }, ); -API.v1.addRoute( - 'users.getAvatar', - { authRequired: false }, - { - get() { - const user = this.getUserFromParams(); - - const url = getURL(`/avatar/${user.username}`, { cdn: false, full: true }); - this.response.setHeader('Location', url); - - return { - statusCode: 307, - body: url, - }; - }, - }, -); - API.v1.addRoute( 'users.setActiveStatus', - { authRequired: true }, + { authRequired: true, validateParams: isUserSetActiveStatusParamsPOST }, { post() { - check(this.bodyParams, { - userId: String, - activeStatus: Boolean, - confirmRelinquish: Match.Maybe(Boolean), - }); - if (!hasPermission(this.userId, 'edit-other-user-active-status')) { return API.v1.unauthorized(); } - Meteor.runAsUser(this.userId, () => { - const { userId, activeStatus, confirmRelinquish = false } = this.bodyParams; - Meteor.call('setUserActiveStatus', userId, activeStatus, confirmRelinquish); - }); + const { userId, activeStatus, confirmRelinquish = false } = this.bodyParams; + Meteor.call('setUserActiveStatus', userId, activeStatus, confirmRelinquish); return API.v1.success({ user: Users.findOneById(this.bodyParams.userId, { fields: { active: 1 } }), }); @@ -171,14 +318,9 @@ API.v1.addRoute( API.v1.addRoute( 'users.deactivateIdle', - { authRequired: true }, + { authRequired: true, validateParams: isUserDeactivateIdleParamsPOST }, { post() { - check(this.bodyParams, { - daysIdle: Match.Integer, - role: Match.Optional(String), - }); - if (!hasPermission(this.userId, 'edit-other-user-active-status')) { return API.v1.unauthorized(); } @@ -197,68 +339,41 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'users.getPresence', - { authRequired: true }, - { - get() { - if (this.isUserFromParams()) { - const user = Users.findOneById(this.userId); - return API.v1.success({ - presence: user.status, - connectionStatus: user.statusConnection, - lastLogin: user.lastLogin, - }); - } - - const user = this.getUserFromParams(); - - return API.v1.success({ - presence: user.status, - }); - }, - }, -); - API.v1.addRoute( 'users.info', - { authRequired: true }, + { authRequired: true, validateParams: isUsersInfoParamsGetProps }, { - get() { - const { username, userId } = this.requestParams(); + async get() { const { fields } = this.parseJsonQuery(); - check(userId, Match.Maybe(String)); - check(username, Match.Maybe(String)); - - if (userId !== undefined && username !== undefined) { - throw new Meteor.Error('invalid-filter', 'Cannot filter by id and username at once'); - } - - if (!userId && !username) { - throw new Meteor.Error('invalid-filter', 'Must filter by id or username'); - } - - const user = getFullUserDataByIdOrUsername({ userId: this.userId, filterId: userId, filterUsername: username }); + const user = await getFullUserDataByIdOrUsername(this.userId, { + filterId: (this.queryParams as any).userId, + filterUsername: (this.queryParams as any).username, + }); if (!user) { return API.v1.failure('User not found.'); } const myself = user._id === this.userId; if (fields.userRooms === 1 && (myself || hasPermission(this.userId, 'view-other-user-channels'))) { - user.rooms = Subscriptions.findByUserId(user._id, { - fields: { - rid: 1, - name: 1, - t: 1, - roles: 1, - unread: 1, - }, - sort: { - t: 1, - name: 1, + return API.v1.success({ + user: { + ...user, + rooms: Subscriptions.findByUserId(user._id, { + projection: { + rid: 1, + name: 1, + t: 1, + roles: 1, + unread: 1, + }, + sort: { + t: 1, + name: 1, + }, + }).fetch(), }, - }).fetch(); + }); } return API.v1.success({ @@ -298,14 +413,14 @@ API.v1.addRoute( inclusiveFieldsKeys.includes('emails') && 'emails.address.*', inclusiveFieldsKeys.includes('username') && 'username.*', inclusiveFieldsKeys.includes('name') && 'name.*', - ].filter(Boolean), + ].filter(Boolean) as string[], this.queryOperations, ) ) { throw new Meteor.Error('error-invalid-query', isValidQuery.errors.join('\n')); } - const actualSort = sort && sort.name ? { nameInsensitive: sort.name, ...sort } : sort || { username: 1 }; + const actualSort = sort?.name ? { nameInsensitive: sort.name, ...sort } : sort || { username: 1 }; const limit = count !== 0 @@ -373,6 +488,7 @@ API.v1.addRoute( numRequestsAllowed: settings.get('Rate_Limiter_Limit_RegisterUser'), intervalTimeInMS: settings.get('API_Enable_Rate_Limiter_Limit_Time_Default'), }, + validateParams: isUserRegisterParamsPOST, }, { post() { @@ -380,303 +496,40 @@ API.v1.addRoute( return API.v1.failure('Logged in users can not register again.'); } - // We set their username here, so require it - // The `registerUser` checks for the other requirements - check( - this.bodyParams, - Match.ObjectIncluding({ - username: String, - }), - ); - if (!checkUsernameAvailability(this.bodyParams.username)) { return API.v1.failure('Username is already in use'); } // Register the user - const userId = Meteor.call('registerUser', this.bodyParams); - - // Now set their username - Meteor.runAsUser(userId, () => Meteor.call('setUsername', this.bodyParams.username)); - const { fields } = this.parseJsonQuery(); - - return API.v1.success({ user: Users.findOneById(userId, { fields }) }); - }, - }, -); - -API.v1.addRoute( - 'users.resetAvatar', - { authRequired: true }, - { - post() { - const user = this.getUserFromParams(); - - if (settings.get('Accounts_AllowUserAvatarChange') && user._id === this.userId) { - Meteor.runAsUser(this.userId, () => Meteor.call('resetAvatar')); - } else if (hasPermission(this.userId, 'edit-other-user-avatar')) { - Meteor.runAsUser(this.userId, () => Meteor.call('resetAvatar', user._id)); - } else { - throw new Meteor.Error('error-not-allowed', 'Reset avatar is not allowed', { - method: 'users.resetAvatar', - }); - } - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'users.setAvatar', - { authRequired: true }, - { - async post() { - check( - this.bodyParams, - Match.ObjectIncluding({ - avatarUrl: Match.Maybe(String), - userId: Match.Maybe(String), - username: Match.Maybe(String), - }), - ); - const canEditOtherUserAvatar = hasPermission(this.userId, 'edit-other-user-avatar'); - - if (!settings.get('Accounts_AllowUserAvatarChange') && !canEditOtherUserAvatar) { - throw new Meteor.Error('error-not-allowed', 'Change avatar is not allowed', { - method: 'users.setAvatar', - }); - } - - let user; - if (this.isUserFromParams()) { - user = Meteor.users.findOne(this.userId); - } else if (canEditOtherUserAvatar) { - user = this.getUserFromParams(); - } else { - return API.v1.unauthorized(); - } - - if (this.bodyParams.avatarUrl) { - setUserAvatar(user, this.bodyParams.avatarUrl, '', 'url'); - return API.v1.success(); - } - - const { image, ...fields } = await getUploadFormData({ - request: this.request, - }); - - if (!image) { - return API.v1.failure("The 'image' param is required"); - } - - const sentTheUserByFormData = fields.userId || fields.username; - if (sentTheUserByFormData) { - if (fields.userId) { - user = Users.findOneById(fields.userId, { fields: { username: 1 } }); - } else if (fields.username) { - user = Users.findOneByUsernameIgnoringCase(fields.username, { fields: { username: 1 } }); - } - - if (!user) { - throw new Meteor.Error('error-invalid-user', 'The optional "userId" or "username" param provided does not match any users'); - } - - const isAnotherUser = this.userId !== user._id; - if (isAnotherUser && !hasPermission(this.userId, 'edit-other-user-avatar')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed'); - } - } - - setUserAvatar(user, image.fileBuffer, image.mimetype, 'rest'); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'users.getStatus', - { authRequired: true }, - { - get() { - if (this.isUserFromParams()) { - const user = Users.findOneById(this.userId); - return API.v1.success({ - _id: user._id, - message: user.statusText, - connectionStatus: user.statusConnection, - status: user.status, - }); - } - - const user = this.getUserFromParams(); - - return API.v1.success({ - _id: user._id, - message: user.statusText, - status: user.status, - }); - }, - }, -); - -API.v1.addRoute( - 'users.setStatus', - { authRequired: true }, - { - post() { - check( - this.bodyParams, - Match.ObjectIncluding({ - status: Match.Maybe(String), - message: Match.Maybe(String), - }), - ); - - if (!settings.get('Accounts_AllowUserStatusMessageChange')) { - throw new Meteor.Error('error-not-allowed', 'Change status is not allowed', { - method: 'users.setStatus', - }); - } - - let user; - if (this.isUserFromParams()) { - user = Meteor.users.findOne(this.userId); - } else if (hasPermission(this.userId, 'edit-other-user-info')) { - user = this.getUserFromParams(); - } else { - return API.v1.unauthorized(); - } - - Meteor.runAsUser(user._id, () => { - if (this.bodyParams.message || this.bodyParams.message === '') { - setStatusText(user._id, this.bodyParams.message); - } - if (this.bodyParams.status) { - const validStatus = ['online', 'away', 'offline', 'busy']; - if (validStatus.includes(this.bodyParams.status)) { - const { status } = this.bodyParams; - - if (status === 'offline' && !settings.get('Accounts_AllowInvisibleStatusOption')) { - throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', { - method: 'users.setStatus', - }); - } - - Meteor.users.update(user._id, { - $set: { - status, - statusDefault: status, - }, - }); - - setUserStatus(user, status); - } else { - throw new Meteor.Error('error-invalid-status', 'Valid status types include online, away, offline, and busy.', { - method: 'users.setStatus', - }); - } - } - }); - - return API.v1.success(); - }, - }, -); - -API.v1.addRoute( - 'users.update', - { authRequired: true, twoFactorRequired: true }, - { - post() { - check(this.bodyParams, { - userId: String, - data: Match.ObjectIncluding({ - email: Match.Maybe(String), - name: Match.Maybe(String), - password: Match.Maybe(String), - username: Match.Maybe(String), - bio: Match.Maybe(String), - nickname: Match.Maybe(String), - statusText: Match.Maybe(String), - active: Match.Maybe(Boolean), - roles: Match.Maybe(Array), - joinDefaultChannels: Match.Maybe(Boolean), - requirePasswordChange: Match.Maybe(Boolean), - sendWelcomeEmail: Match.Maybe(Boolean), - verified: Match.Maybe(Boolean), - customFields: Match.Maybe(Object), - }), - }); - - const userData = _.extend({ _id: this.bodyParams.userId }, this.bodyParams.data); - - Meteor.runAsUser(this.userId, () => saveUser(this.userId, userData)); - - if (this.bodyParams.data.customFields) { - saveCustomFields(this.bodyParams.userId, this.bodyParams.data.customFields); - } - - if (typeof this.bodyParams.data.active !== 'undefined') { - const { - userId, - data: { active }, - confirmRelinquish = false, - } = this.bodyParams; + const userId = Meteor.call('registerUser', this.bodyParams); - Meteor.runAsUser(this.userId, () => { - Meteor.call('setUserActiveStatus', userId, active, confirmRelinquish); - }); - } + // Now set their username + Meteor.runAsUser(userId, () => Meteor.call('setUsername', this.bodyParams.username)); const { fields } = this.parseJsonQuery(); - return API.v1.success({ user: Users.findOneById(this.bodyParams.userId, { fields }) }); + return API.v1.success({ user: Users.findOneById(userId, { fields }) }); }, }, ); API.v1.addRoute( - 'users.updateOwnBasicInfo', + 'users.resetAvatar', { authRequired: true }, { post() { - check(this.bodyParams, { - data: Match.ObjectIncluding({ - email: Match.Maybe(String), - name: Match.Maybe(String), - username: Match.Maybe(String), - nickname: Match.Maybe(String), - statusText: Match.Maybe(String), - currentPassword: Match.Maybe(String), - newPassword: Match.Maybe(String), - }), - customFields: Match.Maybe(Object), - }); - - const userData = { - email: this.bodyParams.data.email, - realname: this.bodyParams.data.name, - username: this.bodyParams.data.username, - nickname: this.bodyParams.data.nickname, - statusText: this.bodyParams.data.statusText, - newPassword: this.bodyParams.data.newPassword, - typedPassword: this.bodyParams.data.currentPassword, - }; - - // saveUserProfile now uses the default two factor authentication procedures, so we need to provide that - const twoFactorOptions = !userData.typedPassword - ? null - : { - twoFactorCode: userData.typedPassword, - twoFactorMethod: 'password', - }; + const user = this.getUserFromParams(); - Meteor.runAsUser(this.userId, () => Meteor.call('saveUserProfile', userData, this.bodyParams.customFields, twoFactorOptions)); + if (settings.get('Accounts_AllowUserAvatarChange') && user._id === this.userId) { + Meteor.runAsUser(this.userId, () => Meteor.call('resetAvatar')); + } else if (hasPermission(this.userId, 'edit-other-user-avatar')) { + Meteor.runAsUser(this.userId, () => Meteor.call('resetAvatar', user._id)); + } else { + throw new Meteor.Error('error-not-allowed', 'Reset avatar is not allowed', { + method: 'users.resetAvatar', + }); + } - return API.v1.success({ - user: Users.findOneById(this.userId, { fields: API.v1.defaultFieldsToExclude }), - }); + return API.v1.success(); }, }, ); @@ -687,10 +540,7 @@ API.v1.addRoute( { post() { const user = this.getUserFromParams(); - let data; - Meteor.runAsUser(this.userId, () => { - data = Meteor.call('createToken', user._id); - }); + const data = Meteor.call('createToken', user._id); return data ? API.v1.success({ data }) : API.v1.unauthorized(); }, }, @@ -715,77 +565,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'users.setPreferences', - { authRequired: true }, - { - post() { - check(this.bodyParams, { - userId: Match.Maybe(String), - data: Match.ObjectIncluding({ - newRoomNotification: Match.Maybe(String), - newMessageNotification: Match.Maybe(String), - clockMode: Match.Maybe(Number), - useEmojis: Match.Maybe(Boolean), - convertAsciiEmoji: Match.Maybe(Boolean), - saveMobileBandwidth: Match.Maybe(Boolean), - collapseMediaByDefault: Match.Maybe(Boolean), - autoImageLoad: Match.Maybe(Boolean), - emailNotificationMode: Match.Maybe(String), - unreadAlert: Match.Maybe(Boolean), - notificationsSoundVolume: Match.Maybe(Number), - desktopNotifications: Match.Maybe(String), - pushNotifications: Match.Maybe(String), - enableAutoAway: Match.Maybe(Boolean), - highlights: Match.Maybe(Array), - desktopNotificationRequireInteraction: Match.Maybe(Boolean), - messageViewMode: Match.Maybe(Number), - showMessageInMainThread: Match.Maybe(Boolean), - hideUsernames: Match.Maybe(Boolean), - hideRoles: Match.Maybe(Boolean), - displayAvatars: Match.Maybe(Boolean), - hideFlexTab: Match.Maybe(Boolean), - sendOnEnter: Match.Maybe(String), - language: Match.Maybe(String), - sidebarShowFavorites: Match.Optional(Boolean), - sidebarShowUnread: Match.Optional(Boolean), - sidebarSortby: Match.Optional(String), - sidebarViewMode: Match.Optional(String), - sidebarDisplayAvatar: Match.Optional(Boolean), - sidebarGroupByType: Match.Optional(Boolean), - muteFocusedConversations: Match.Optional(Boolean), - }), - }); - if (this.bodyParams.userId && this.bodyParams.userId !== this.userId && !hasPermission(this.userId, 'edit-other-user-info')) { - throw new Meteor.Error('error-action-not-allowed', 'Editing user is not allowed'); - } - const userId = this.bodyParams.userId ? this.bodyParams.userId : this.userId; - if (!Users.findOneById(userId)) { - throw new Meteor.Error('error-invalid-user', 'The optional "userId" param provided does not match any users'); - } - - Meteor.runAsUser(userId, () => Meteor.call('saveUserPreferences', this.bodyParams.data)); - const user = Users.findOneById(userId, { - fields: { - 'settings.preferences': 1, - 'language': 1, - }, - }); - return API.v1.success({ - user: { - _id: user._id, - settings: { - preferences: { - ...user.settings.preferences, - language: user.language, - }, - }, - }, - }); - }, - }, -); - API.v1.addRoute( 'users.forgotPassword', { authRequired: false }, @@ -807,7 +586,7 @@ API.v1.addRoute( { authRequired: true }, { get() { - const result = Meteor.runAsUser(this.userId, () => Meteor.call('getUsernameSuggestion')); + const result = Meteor.call('getUsernameSuggestion'); return API.v1.success({ result }); }, @@ -823,7 +602,7 @@ API.v1.addRoute( if (!tokenName) { return API.v1.failure("The 'tokenName' param is required"); } - const token = Meteor.runAsUser(this.userId, () => Meteor.call('personalAccessTokens:generateToken', { tokenName, bypassTwoFactor })); + const token = Meteor.call('personalAccessTokens:generateToken', { tokenName, bypassTwoFactor }); return API.v1.success({ token }); }, @@ -839,7 +618,7 @@ API.v1.addRoute( if (!tokenName) { return API.v1.failure("The 'tokenName' param is required"); } - const token = Meteor.runAsUser(this.userId, () => Meteor.call('personalAccessTokens:regenerateToken', { tokenName })); + const token = Meteor.call('personalAccessTokens:regenerateToken', { tokenName }); return API.v1.success({ token }); }, @@ -854,19 +633,19 @@ API.v1.addRoute( if (!hasPermission(this.userId, 'create-personal-access-tokens')) { throw new Meteor.Error('not-authorized', 'Not Authorized'); } - const loginTokens = Users.getLoginTokensByUserId(this.userId).fetch()[0]; - const getPersonalAccessTokens = () => - loginTokens.services.resume.loginTokens - .filter((loginToken) => loginToken.type && loginToken.type === 'personalAccessToken') - .map((loginToken) => ({ - name: loginToken.name, - createdAt: loginToken.createdAt, - lastTokenPart: loginToken.lastTokenPart, - bypassTwoFactor: loginToken.bypassTwoFactor, - })); + + const user = Users.getLoginTokensByUserId(this.userId).fetch()[0] as IUser | undefined; return API.v1.success({ - tokens: loginTokens ? getPersonalAccessTokens() : [], + tokens: + user?.services?.resume?.loginTokens + ?.filter((loginToken: any) => loginToken.type === 'personalAccessToken') + .map((loginToken: IPersonalAccessToken) => ({ + name: loginToken.name, + createdAt: loginToken.createdAt.toISOString(), + lastTokenPart: loginToken.lastTokenPart, + bypassTwoFactor: Boolean(loginToken.bypassTwoFactor), + })) || [], }); }, }, @@ -881,11 +660,9 @@ API.v1.addRoute( if (!tokenName) { return API.v1.failure("The 'tokenName' param is required"); } - Meteor.runAsUser(this.userId, () => - Meteor.call('personalAccessTokens:removeToken', { - tokenName, - }), - ); + Meteor.call('personalAccessTokens:removeToken', { + tokenName, + }); return API.v1.success(); }, @@ -928,7 +705,7 @@ API.v1.addRoute('users.2fa.sendEmailCode', { const userId = this.userId || Users[method](emailOrUsername, { fields: { _id: 1 } })?._id; if (!userId) { - this.logger.error('[2fa] User was not found when requesting 2fa email code'); + // this.logger.error('[2fa] User was not found when requesting 2fa email code'); return API.v1.success(); } @@ -965,7 +742,7 @@ API.v1.addRoute( if (from) { const ts = new Date(from); - const diff = (Date.now() - ts) / 1000 / 60; + const diff = (Date.now() - Number(ts)) / 1000 / 60; if (diff < 10) { return API.v1.success({ @@ -989,10 +766,13 @@ API.v1.addRoute( { get() { const { fullExport = false } = this.queryParams; - const result = Meteor.runAsUser(this.userId, () => Meteor.call('requestDataDownload', { fullExport: fullExport === 'true' })); + const result = Meteor.call('requestDataDownload', { fullExport: fullExport === 'true' }) as { + requested: boolean; + exportOperation: IExportOperation; + }; return API.v1.success({ - requested: result.requested, + requested: Boolean(result.requested), exportOperation: result.exportOperation, }); }, @@ -1004,48 +784,43 @@ API.v1.addRoute( { authRequired: true }, { async post() { - try { - const hashedToken = Accounts._hashLoginToken(this.request.headers['x-auth-token']); + const xAuthToken = this.request.headers['x-auth-token'] as string; - if (!(await UsersRaw.removeNonPATLoginTokensExcept(this.userId, hashedToken))) { - throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); - } + if (!xAuthToken) { + throw new Meteor.Error('error-parameter-required', 'x-auth-token is required'); + } + const hashedToken = Accounts._hashLoginToken(xAuthToken); - const me = await UsersRaw.findOneById(this.userId, { projection: { 'services.resume.loginTokens': 1 } }); + if (!(await UsersRaw.removeNonPATLoginTokensExcept(this.userId, hashedToken))) { + throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); + } - const token = me.services.resume.loginTokens.find((token) => token.hashedToken === hashedToken); + const me = (await UsersRaw.findOneById(this.userId, { projection: { 'services.resume.loginTokens': 1 } })) as Pick; - const tokenExpires = new Date(token.when.getTime() + settings.get('Accounts_LoginExpiration') * 1000); + const token = me.services?.resume?.loginTokens?.find((token) => token.hashedToken === hashedToken); - return API.v1.success({ - token: this.request.headers['x-auth-token'], - tokenExpires, - }); - } catch (error) { - return API.v1.failure(error); - } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const tokenExpires = new Date(token!.when.getTime() + settings.get('Accounts_LoginExpiration') * 1000); + + return API.v1.success({ + token: xAuthToken, + tokenExpires: tokenExpires.toISOString() || '', + }); }, }, ); API.v1.addRoute( 'users.autocomplete', - { authRequired: true }, + { authRequired: true, validateParams: isUsersAutocompleteProps }, { - get() { + async get() { const { selector } = this.queryParams; - - if (!selector) { - return API.v1.failure("The 'selector' param is required"); - } - return API.v1.success( - Promise.await( - findUsersToAutocomplete({ - uid: this.userId, - selector: JSON.parse(selector), - }), - ), + await findUsersToAutocomplete({ + uid: this.userId, + selector: JSON.parse(selector), + }), ); }, }, @@ -1056,7 +831,7 @@ API.v1.addRoute( { authRequired: true }, { post() { - API.v1.success(Meteor.call('removeOtherTokens')); + return API.v1.success(Meteor.call('removeOtherTokens')); }, }, ); @@ -1066,30 +841,28 @@ API.v1.addRoute( { authRequired: true, twoFactorRequired: true, twoFactorOptions: { disableRememberMe: true } }, { post() { - // reset own keys - if (this.isUserFromParams()) { - resetUserE2EEncriptionKey(this.userId, false); - return API.v1.success(); - } + if ('userId' in this.bodyParams || 'username' in this.bodyParams || 'user' in this.bodyParams) { + // reset other user keys + const user = this.getUserFromParams(); + if (!user) { + throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); + } - // reset other user keys - const user = this.getUserFromParams(); - if (!user) { - throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); - } + if (!settings.get('Accounts_TwoFactorAuthentication_Enforce_Password_Fallback')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } - if (!settings.get('Accounts_TwoFactorAuthentication_Enforce_Password_Fallback')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed'); - } + if (!hasPermission(this.userId, 'edit-other-user-e2ee')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } - if (!hasPermission(Meteor.userId(), 'edit-other-user-e2ee')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed'); - } + if (!resetUserE2EEncriptionKey(user._id, true)) { + return API.v1.failure(); + } - if (!resetUserE2EEncriptionKey(user._id, true)) { - return API.v1.failure(); + return API.v1.success(); } - + resetUserE2EEncriptionKey(this.userId, false); return API.v1.success(); }, }, @@ -1099,29 +872,28 @@ API.v1.addRoute( 'users.resetTOTP', { authRequired: true, twoFactorRequired: true, twoFactorOptions: { disableRememberMe: true } }, { - post() { - // reset own keys - if (this.isUserFromParams()) { - Promise.await(resetTOTP(this.userId, false)); - return API.v1.success(); - } - - // reset other user keys - const user = this.getUserFromParams(); - if (!user) { - throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); - } + async post() { + // // reset own keys + if ('userId' in this.bodyParams || 'username' in this.bodyParams || 'user' in this.bodyParams) { + // reset other user keys + if (!hasPermission(this.userId, 'edit-other-user-totp')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } - if (!settings.get('Accounts_TwoFactorAuthentication_Enforce_Password_Fallback')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed'); - } + if (!settings.get('Accounts_TwoFactorAuthentication_Enforce_Password_Fallback')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed'); + } - if (!hasPermission(Meteor.userId(), 'edit-other-user-totp')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed'); - } + const user = this.getUserFromParams(); + if (!user) { + throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); + } - Promise.await(resetTOTP(user._id, true)); + await resetTOTP(user._id, true); + return API.v1.success(); + } + await resetTOTP(this.userId, false); return API.v1.success(); }, }, @@ -1129,25 +901,22 @@ API.v1.addRoute( API.v1.addRoute( 'users.listTeams', - { authRequired: true }, + { authRequired: true, validateParams: isUsersListTeamsProps }, { - get() { + async get() { check( this.queryParams, Match.ObjectIncluding({ userId: Match.Maybe(String), }), ); - const { userId } = this.queryParams; - if (!userId) { - throw new Meteor.Error('error-invalid-user-id', 'Invalid user id'); - } + const { userId } = this.queryParams; // If the caller has permission to view all teams, there's no need to filter the teams const adminId = hasPermission(this.userId, 'view-all-teams') ? undefined : this.userId; - const teams = Promise.await(Team.findBySubscribedUserIds(userId, adminId)); + const teams = await Team.findBySubscribedUserIds(userId, adminId); return API.v1.success({ teams, @@ -1158,7 +927,7 @@ API.v1.addRoute( API.v1.addRoute( 'users.logout', - { authRequired: true }, + { authRequired: true, validateParams: isUserLogoutParamsPOST }, { post() { const userId = this.bodyParams.userId || this.userId; @@ -1179,7 +948,130 @@ API.v1.addRoute( }, ); -settings.watch('Rate_Limiter_Limit_RegisterUser', (value) => { +API.v1.addRoute( + 'users.getPresence', + { authRequired: true }, + { + get() { + if (this.isUserFromParams()) { + const user = Users.findOneById(this.userId); + return API.v1.success({ + presence: user.status || 'offline', + connectionStatus: user.statusConnection || 'offline', + ...(user.lastLogin && { lastLogin: user.lastLogin }), + }); + } + + const user = this.getUserFromParams(); + + return API.v1.success({ + presence: user.status || 'offline', + }); + }, + }, +); + +API.v1.addRoute( + 'users.setStatus', + { authRequired: true }, + { + post() { + check( + this.bodyParams, + Match.ObjectIncluding({ + status: Match.Maybe(String), + message: Match.Maybe(String), + }), + ); + + if (!settings.get('Accounts_AllowUserStatusMessageChange')) { + throw new Meteor.Error('error-not-allowed', 'Change status is not allowed', { + method: 'users.setStatus', + }); + } + + const user = ((): IUser | undefined => { + if (this.isUserFromParams()) { + return Meteor.users.findOne(this.userId) as IUser; + } + if (hasPermission(this.userId, 'edit-other-user-info')) { + return this.getUserFromParams(); + } + })(); + + if (user === undefined) { + return API.v1.unauthorized(); + } + + Meteor.runAsUser(user._id, () => { + if (this.bodyParams.message || this.bodyParams.message === '') { + setStatusText(user._id, this.bodyParams.message); + } + if (this.bodyParams.status) { + const validStatus = ['online', 'away', 'offline', 'busy']; + if (validStatus.includes(this.bodyParams.status)) { + const { status } = this.bodyParams; + + if (status === 'offline' && !settings.get('Accounts_AllowInvisibleStatusOption')) { + throw new Meteor.Error('error-status-not-allowed', 'Invisible status is disabled', { + method: 'users.setStatus', + }); + } + + Meteor.users.update(user._id, { + $set: { + status, + statusDefault: status, + }, + }); + + setUserStatus(user, status); + } else { + throw new Meteor.Error('error-invalid-status', 'Valid status types include online, away, offline, and busy.', { + method: 'users.setStatus', + }); + } + } + }); + + return API.v1.success(); + }, + }, +); + +// status: 'online' | 'offline' | 'away' | 'busy'; +// message?: string; +// _id: string; +// connectionStatus?: 'online' | 'offline' | 'away' | 'busy'; +// }; + +API.v1.addRoute( + 'users.getStatus', + { authRequired: true }, + { + get() { + if (this.isUserFromParams()) { + const user = Users.findOneById(this.userId); + return API.v1.success({ + _id: user._id, + // message: user.statusText, + connectionStatus: (user.statusConnection || 'offline') as 'online' | 'offline' | 'away' | 'busy', + status: (user.status || 'offline') as 'online' | 'offline' | 'away' | 'busy', + }); + } + + const user = this.getUserFromParams(); + + return API.v1.success({ + _id: user._id, + // message: user.statusText, + status: (user.status || 'offline') as 'online' | 'offline' | 'away' | 'busy', + }); + }, + }, +); + +settings.watch('Rate_Limiter_Limit_RegisterUser', (value) => { const userRegisterRoute = '/api/v1/users.registerpost'; API.v1.updateRateLimiterDictionaryForRoute(userRegisterRoute, value); diff --git a/apps/meteor/app/apps/client/@types/IOrchestrator.ts b/apps/meteor/app/apps/client/@types/IOrchestrator.ts index 55400407a9462..848dcdfabf821 100644 --- a/apps/meteor/app/apps/client/@types/IOrchestrator.ts +++ b/apps/meteor/app/apps/client/@types/IOrchestrator.ts @@ -1,4 +1,5 @@ import { ISetting } from '@rocket.chat/apps-engine/definition/settings/ISetting'; +import { App } from '@rocket.chat/core-typings'; export interface IDetailedDescription { raw: string; @@ -109,26 +110,6 @@ export enum EAppPurchaseType { PurchaseTypeSubscription = 'subscription', } -export interface IAppFromMarketplace { - appId: string; - latest: ILatest; - isAddon: boolean; - isEnterpriseOnly: boolean; - isBundle: boolean; - bundledAppIds: any[]; - bundledIn: IBundledIn[]; - isPurchased: boolean; - isSubscribed: boolean; - subscriptionInfo: ISubscriptionInfo; - price: number; - purchaseType: EAppPurchaseType; - isUsageBased: boolean; - createdAt: Date; - modifiedAt: Date; - pricingPlans: IPricingPlan[]; - addonId: string; -} - export interface ILanguageInfo { Params: string; Description: string; @@ -163,7 +144,7 @@ export interface ICategory { // } export interface IAppSynced { - app: IAppFromMarketplace; + app: App; success: boolean; } diff --git a/apps/meteor/app/apps/client/orchestrator.ts b/apps/meteor/app/apps/client/orchestrator.ts index ea224409fbc4e..155027a31f942 100644 --- a/apps/meteor/app/apps/client/orchestrator.ts +++ b/apps/meteor/app/apps/client/orchestrator.ts @@ -15,8 +15,6 @@ import { settings } from '../../settings/client'; import { CachedCollectionManager } from '../../ui-cached-collection'; import { createDeferredValue } from '../lib/misc/DeferredValue'; import { - IPricingPlan, - EAppPurchaseType, // IAppFromMarketplace, IAppLanguage, IAppExternalURL, @@ -24,10 +22,6 @@ import { // IAppSynced, // IAppScreenshots, // IScreenshot, - IAuthor, - IDetailedChangelog, - IDetailedDescription, - ISubscriptionInfo, } from './@types/IOrchestrator'; import { AppWebsocketReceiver } from './communication'; import { handleI18nResources } from './i18n'; @@ -35,42 +29,6 @@ import { RealAppsEngineUIHost } from './RealAppsEngineUIHost'; import { APIClient } from '../../utils/client'; import { hasAtLeastOnePermission } from '../../authorization/client'; -export interface IAppsFromMarketplace { - price: number; - pricingPlans: IPricingPlan[]; - purchaseType: EAppPurchaseType; - isEnterpriseOnly: boolean; - modifiedAt: Date; - internalId: string; - id: string; - name: string; - nameSlug: string; - version: string; - categories: string[]; - description: string; - detailedDescription: IDetailedDescription; - detailedChangelog: IDetailedChangelog; - requiredApiVersion: string; - author: IAuthor; - classFile: string; - iconFile: string; - iconFileData: string; - status: string; - isVisible: boolean; - createdDate: Date; - modifiedDate: Date; - isPurchased: boolean; - isSubscribed: boolean; - subscriptionInfo: ISubscriptionInfo; - compiled: boolean; - compileJobId: string; - changesNote: string; - languages: string[]; - privacyPolicySummary: string; - internalChangesNote: string; - permissions: IPermission[]; -} - class AppClientOrchestrator { private _appClientUIHost: RealAppsEngineUIHost; diff --git a/apps/meteor/app/apps/server/bridges/listeners.js b/apps/meteor/app/apps/server/bridges/listeners.js index 57e0a9b63bba4..ced6e1aea31b8 100644 --- a/apps/meteor/app/apps/server/bridges/listeners.js +++ b/apps/meteor/app/apps/server/bridges/listeners.js @@ -7,6 +7,7 @@ export class AppListenerBridge { } async handleEvent(event, ...payload) { + // eslint-disable-next-line complexity const method = (() => { switch (event) { case AppInterface.IPreMessageSentPrevent: diff --git a/apps/meteor/app/apps/server/communication/index.js b/apps/meteor/app/apps/server/communication/index.ts similarity index 100% rename from apps/meteor/app/apps/server/communication/index.js rename to apps/meteor/app/apps/server/communication/index.ts diff --git a/apps/meteor/app/apps/server/communication/methods.js b/apps/meteor/app/apps/server/communication/methods.ts similarity index 55% rename from apps/meteor/app/apps/server/communication/methods.js rename to apps/meteor/app/apps/server/communication/methods.ts index 1bccb4fbed460..a469408146b24 100644 --- a/apps/meteor/app/apps/server/communication/methods.js +++ b/apps/meteor/app/apps/server/communication/methods.ts @@ -1,27 +1,27 @@ import { Meteor } from 'meteor/meteor'; +import { SettingValue } from '@rocket.chat/core-typings'; import { Settings } from '../../../models/server/raw'; import { hasPermission } from '../../../authorization/server'; import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; +import { AppServerOrchestrator } from '../orchestrator'; -const waitToLoad = function (orch) { - return new Promise((resolve) => { - let id = setInterval(() => { +const waitToLoad = function (orch: AppServerOrchestrator): unknown { + return new Promise((resolve) => { + const id = setInterval(() => { if (orch.isEnabled() && orch.isLoaded()) { clearInterval(id); - id = -1; resolve(); } }, 100); }); }; -const waitToUnload = function (orch) { - return new Promise((resolve) => { - let id = setInterval(() => { +const waitToUnload = function (orch: AppServerOrchestrator): unknown { + return new Promise((resolve) => { + const id = setInterval(() => { if (!orch.isEnabled() && !orch.isLoaded()) { clearInterval(id); - id = -1; resolve(); } }, 100); @@ -29,21 +29,24 @@ const waitToUnload = function (orch) { }; export class AppMethods { - constructor(orch) { - this._orch = orch; + private orch: AppServerOrchestrator; - this._addMethods(); + constructor(orch: AppServerOrchestrator) { + this.orch = orch; + + this.addMethods(); } - isEnabled() { - return typeof this._orch !== 'undefined' && this._orch.isEnabled(); + isEnabled(): SettingValue { + return typeof this.orch !== 'undefined' && this.orch.isEnabled(); } - isLoaded() { - return typeof this._orch !== 'undefined' && this._orch.isEnabled() && this._orch.isLoaded(); + isLoaded(): boolean { + return Boolean(typeof this.orch !== 'undefined' && this.orch.isEnabled() && this.orch.isLoaded()); } - _addMethods() { + private addMethods(): void { + // eslint-disable-next-line @typescript-eslint/no-this-alias const instance = this; Meteor.methods({ @@ -56,13 +59,14 @@ export class AppMethods { }, 'apps/go-enable': twoFactorRequired(function _appsGoEnable() { - if (!Meteor.userId()) { + const uid = Meteor.userId(); + if (!uid) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'apps/go-enable', }); } - if (!hasPermission(Meteor.userId(), 'manage-apps')) { + if (!hasPermission(uid, 'manage-apps')) { throw new Meteor.Error('error-action-not-allowed', 'Not allowed', { method: 'apps/go-enable', }); @@ -70,17 +74,18 @@ export class AppMethods { Settings.updateValueById('Apps_Framework_enabled', true); - Promise.await(waitToLoad(instance._orch)); + Promise.await(waitToLoad(instance.orch)); }), 'apps/go-disable': twoFactorRequired(function _appsGoDisable() { - if (!Meteor.userId()) { + const uid = Meteor.userId(); + if (!uid) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'apps/go-enable', }); } - if (!hasPermission(Meteor.userId(), 'manage-apps')) { + if (!hasPermission(uid, 'manage-apps')) { throw new Meteor.Error('error-action-not-allowed', 'Not allowed', { method: 'apps/go-enable', }); @@ -88,7 +93,7 @@ export class AppMethods { Settings.updateValueById('Apps_Framework_enabled', false); - Promise.await(waitToUnload(instance._orch)); + Promise.await(waitToUnload(instance.orch)); }), }); } diff --git a/apps/meteor/app/apps/server/communication/rest.js b/apps/meteor/app/apps/server/communication/rest.js index 2bc64e1f8be58..0b7122e375210 100644 --- a/apps/meteor/app/apps/server/communication/rest.js +++ b/apps/meteor/app/apps/server/communication/rest.js @@ -213,10 +213,13 @@ export class AppsRestApi { return API.v1.failure({ error: 'Direct installation of an App is disabled.' }); } - const formData = await getUploadFormData({ - request: this.request, - }); - buff = formData?.app?.fileBuffer; + const [app, formData] = await getUploadFormData( + { + request: this.request, + }, + { field: 'app' }, + ); + buff = app?.fileBuffer; permissionsGranted = (() => { try { const permissions = JSON.parse(formData?.permissions || ''); @@ -462,10 +465,13 @@ export class AppsRestApi { return API.v1.failure({ error: 'Direct updating of an App is disabled.' }); } - const formData = await getUploadFormData({ - request: this.request, - }); - buff = formData?.app?.fileBuffer; + const [app, formData] = await getUploadFormData( + { + request: this.request, + }, + { field: 'app' }, + ); + buff = app?.fileBuffer; permissionsGranted = (() => { try { const permissions = JSON.parse(formData?.permissions || ''); diff --git a/apps/meteor/app/apps/server/communication/uikit.js b/apps/meteor/app/apps/server/communication/uikit.js deleted file mode 100644 index 27b7c906f8cff..0000000000000 --- a/apps/meteor/app/apps/server/communication/uikit.js +++ /dev/null @@ -1,321 +0,0 @@ -import express from 'express'; -import cors from 'cors'; -import rateLimit from 'express-rate-limit'; -import { Meteor } from 'meteor/meteor'; -import { WebApp } from 'meteor/webapp'; -import { UIKitIncomingInteractionType } from '@rocket.chat/apps-engine/definition/uikit'; -import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata'; - -import { Users } from '../../../models/server'; -import { settings } from '../../../settings/server'; -import { Apps } from '../orchestrator'; -import { UiKitCoreApp } from '../../../../server/sdk'; - -const apiServer = express(); - -apiServer.disable('x-powered-by'); - -let corsEnabled = false; -let allowListOrigins = []; - -settings.watch('API_Enable_CORS', (value) => { - corsEnabled = value; -}); - -settings.watch('API_CORS_Origin', (value) => { - allowListOrigins = value - ? value - .trim() - .split(',') - .map((origin) => String(origin).trim().toLocaleLowerCase()) - : []; -}); - -const corsOptions = { - origin: (origin, callback) => { - if ( - !origin || - !corsEnabled || - allowListOrigins.includes('*') || - allowListOrigins.includes(origin) || - origin === settings.get('Site_Url') - ) { - callback(null, true); - } else { - callback('Not allowed by CORS', false); - } - }, -}; - -WebApp.connectHandlers.use(apiServer); - -// eslint-disable-next-line new-cap -const router = express.Router(); - -const unauthorized = (res) => - res.status(401).send({ - status: 'error', - message: 'You must be logged in to do this.', - }); - -Meteor.startup(() => { - // use specific rate limit of 600 (which is 60 times the default limits) requests per minute (around 10/second) - const apiLimiter = rateLimit({ - windowMs: settings.get('API_Enable_Rate_Limiter_Limit_Time_Default'), - max: settings.get('API_Enable_Rate_Limiter_Limit_Calls_Default') * 60, - skip: () => - settings.get('API_Enable_Rate_Limiter') !== true || - (process.env.NODE_ENV === 'development' && settings.get('API_Enable_Rate_Limiter_Dev') !== true), - }); - router.use(apiLimiter); -}); - -router.use((req, res, next) => { - const { 'x-user-id': userId, 'x-auth-token': authToken, 'x-visitor-token': visitorToken } = req.headers; - - if (userId && authToken) { - req.user = Users.findOneByIdAndLoginToken(userId, authToken); - req.userId = req.user._id; - } - - if (visitorToken) { - req.visitor = Apps.getConverters().get('visitors').convertByToken(visitorToken); - } - - if (!req.user && !req.visitor) { - return unauthorized(res); - } - - next(); -}); - -apiServer.use('/api/apps/ui.interaction/', cors(corsOptions), router); - -const getPayloadForType = (type, req) => { - if (type === UIKitIncomingInteractionType.BLOCK) { - const { type, actionId, triggerId, mid, rid, payload, container } = req.body; - - const { visitor, user } = req; - const room = rid; // orch.getConverters().get('rooms').convertById(rid); - const message = mid; - - return { - type, - container, - actionId, - message, - triggerId, - payload, - user, - visitor, - room, - }; - } - - if (type === UIKitIncomingInteractionType.VIEW_CLOSED) { - const { - type, - actionId, - payload: { view, isCleared }, - } = req.body; - - const { user } = req; - - return { - type, - actionId, - user, - payload: { - view, - isCleared, - }, - }; - } - - if (type === UIKitIncomingInteractionType.VIEW_SUBMIT) { - const { type, actionId, triggerId, payload } = req.body; - - const { user } = req; - - return { - type, - actionId, - triggerId, - payload, - user, - }; - } - - throw new Error('Type not supported'); -}; - -router.post('/:appId', async (req, res, next) => { - const { appId } = req.params; - - const isCore = await UiKitCoreApp.isRegistered(appId); - if (!isCore) { - return next(); - } - - const { type } = req.body; - - try { - const payload = { - ...getPayloadForType(type, req), - appId, - }; - - const result = await UiKitCoreApp[type](payload); - - res.send(result); - } catch (e) { - console.error('ops', e); - res.status(500).send({ error: e.message }); - } -}); - -const appsRoutes = (orch) => (req, res) => { - const { appId } = req.params; - - const { type } = req.body; - - switch (type) { - case UIKitIncomingInteractionType.BLOCK: { - const { type, actionId, triggerId, mid, rid, payload, container } = req.body; - - const { visitor } = req; - const room = orch.getConverters().get('rooms').convertById(rid); - const user = orch.getConverters().get('users').convertToApp(req.user); - const message = mid && orch.getConverters().get('messages').convertById(mid); - - const action = { - type, - container, - appId, - actionId, - message, - triggerId, - payload, - user, - visitor, - room, - }; - - try { - const eventInterface = !visitor ? AppInterface.IUIKitInteractionHandler : AppInterface.IUIKitLivechatInteractionHandler; - - const result = Promise.await(orch.triggerEvent(eventInterface, action)); - - res.send(result); - } catch (e) { - res.status(500).send(e.message); - } - break; - } - - case UIKitIncomingInteractionType.VIEW_CLOSED: { - const { - type, - actionId, - payload: { view, isCleared }, - } = req.body; - - const user = orch.getConverters().get('users').convertToApp(req.user); - - const action = { - type, - appId, - actionId, - user, - payload: { - view, - isCleared, - }, - }; - - try { - Promise.await(orch.triggerEvent('IUIKitInteractionHandler', action)); - - res.sendStatus(200); - } catch (e) { - res.status(500).send(e.message); - } - break; - } - - case UIKitIncomingInteractionType.VIEW_SUBMIT: { - const { type, actionId, triggerId, payload } = req.body; - - const user = orch.getConverters().get('users').convertToApp(req.user); - - const action = { - type, - appId, - actionId, - triggerId, - payload, - user, - }; - - try { - const result = Promise.await(orch.triggerEvent('IUIKitInteractionHandler', action)); - - res.send(result); - } catch (e) { - res.status(500).send(e.message); - } - break; - } - - case UIKitIncomingInteractionType.ACTION_BUTTON: { - const { - type, - actionId, - triggerId, - rid, - mid, - payload: { context }, - } = req.body; - - const room = orch.getConverters().get('rooms').convertById(rid); - const user = orch.getConverters().get('users').convertToApp(req.user); - const message = mid && orch.getConverters().get('messages').convertById(mid); - - const action = { - type, - appId, - actionId, - triggerId, - user, - room, - message, - payload: { - context, - }, - }; - - try { - const result = Promise.await(orch.triggerEvent('IUIKitInteractionHandler', action)); - - res.send(result); - } catch (e) { - res.status(500).send(e.message); - } - break; - } - - default: { - res.status(400).send({ error: 'Unknown action' }); - } - } - - // TODO: validate payloads per type -}; - -export class AppUIKitInteractionApi { - constructor(orch) { - this.orch = orch; - - router.post('/:appId', appsRoutes(orch)); - } -} diff --git a/apps/meteor/app/apps/server/communication/uikit.ts b/apps/meteor/app/apps/server/communication/uikit.ts new file mode 100644 index 0000000000000..266cc3f841a32 --- /dev/null +++ b/apps/meteor/app/apps/server/communication/uikit.ts @@ -0,0 +1,326 @@ +import express, { Request, Response } from 'express'; +import cors from 'cors'; +import rateLimit from 'express-rate-limit'; +import { Meteor } from 'meteor/meteor'; +import { WebApp } from 'meteor/webapp'; +import { UIKitIncomingInteractionType } from '@rocket.chat/apps-engine/definition/uikit'; +import { AppInterface } from '@rocket.chat/apps-engine/definition/metadata'; + +import { Users } from '../../../models/server'; +import { settings } from '../../../settings/server'; +import { Apps, AppServerOrchestrator } from '../orchestrator'; +import { UiKitCoreApp } from '../../../../server/sdk'; + +const apiServer = express(); + +apiServer.disable('x-powered-by'); + +let corsEnabled = false; +let allowListOrigins: string[] = []; + +settings.watch('API_Enable_CORS', (value: boolean) => { + corsEnabled = value; +}); + +settings.watch('API_CORS_Origin', (value: string) => { + allowListOrigins = value + ? value + .trim() + .split(',') + .map((origin) => String(origin).trim().toLocaleLowerCase()) + : []; +}); + +WebApp.connectHandlers.use(apiServer); + +// eslint-disable-next-line new-cap +const router = express.Router(); + +const unauthorized = (res: Response): unknown => + res.status(401).send({ + status: 'error', + message: 'You must be logged in to do this.', + }); + +Meteor.startup(() => { + // use specific rate limit of 600 (which is 60 times the default limits) requests per minute (around 10/second) + const apiLimiter = rateLimit({ + windowMs: settings.get('API_Enable_Rate_Limiter_Limit_Time_Default'), + max: (settings.get('API_Enable_Rate_Limiter_Limit_Calls_Default') as number) * 60, + skip: () => + settings.get('API_Enable_Rate_Limiter') !== true || + (process.env.NODE_ENV === 'development' && settings.get('API_Enable_Rate_Limiter_Dev') !== true), + }); + router.use(apiLimiter); +}); + +router.use((req, res, next) => { + const { 'x-user-id': userId, 'x-auth-token': authToken, 'x-visitor-token': visitorToken } = req.headers; + + if (userId && authToken) { + req.body.user = Users.findOneByIdAndLoginToken(userId, authToken); + req.body.userId = req.body.user._id; + } + + if (visitorToken) { + req.body.visitor = Apps.getConverters()?.get('visitors').convertByToken(visitorToken); + } + + if (!req.body.user && !req.body.visitor) { + return unauthorized(res); + } + + next(); +}); + +const corsOptions = { + origin: (origin: string | undefined, callback: Function): void => { + if ( + !origin || + !corsEnabled || + allowListOrigins.includes('*') || + allowListOrigins.includes(origin) || + origin === settings.get('Site_Url') + ) { + callback(null, true); + } else { + callback('Not allowed by CORS', false); + } + }, +}; + +apiServer.use('/api/apps/ui.interaction/', cors(corsOptions), router); // didn't have the rateLimiter option + +const getPayloadForType = (type: UIKitIncomingInteractionType, req: Request): {} => { + if (type === UIKitIncomingInteractionType.BLOCK) { + const { type, actionId, triggerId, mid, rid, payload, container } = req.body; + + const { visitor, user } = req.body; + const room = rid; // orch.getConverters().get('rooms').convertById(rid); + const message = mid; + + return { + type, + container, + actionId, + message, + triggerId, + payload, + user, + visitor, + room, + }; + } + + if (type === UIKitIncomingInteractionType.VIEW_CLOSED) { + const { + type, + actionId, + payload: { view, isCleared }, + } = req.body; + + const { user } = req.body; + + return { + type, + actionId, + user, + payload: { + view, + isCleared, + }, + }; + } + + if (type === UIKitIncomingInteractionType.VIEW_SUBMIT) { + const { type, actionId, triggerId, payload } = req.body; + + const { user } = req.body; + + return { + type, + actionId, + triggerId, + payload, + user, + }; + } + + throw new Error('Type not supported'); +}; + +router.post('/:appId', async (req, res, next) => { + const { appId } = req.params; + + const isCore = await UiKitCoreApp.isRegistered(appId); + if (!isCore) { + return next(); + } + + // eslint-disable-next-line prefer-destructuring + const type: UIKitIncomingInteractionType = req.body.type; + + try { + const payload = { + ...getPayloadForType(type, req), + appId, + }; + + const result = await (UiKitCoreApp as any)[type](payload); // TO-DO: fix type + + res.send(result); + } catch (e) { + if (e instanceof Error) res.status(500).send({ error: e.message }); + else res.status(500).send({ error: e }); + } +}); + +const appsRoutes = + (orch: AppServerOrchestrator) => + (req: Request, res: Response): void => { + const { appId } = req.params; + + const { type } = req.body; + + switch (type) { + case UIKitIncomingInteractionType.BLOCK: { + const { type, actionId, triggerId, mid, rid, payload, container } = req.body; + + const { visitor } = req.body; + const room = orch.getConverters()?.get('rooms').convertById(rid); + const user = orch.getConverters()?.get('users').convertToApp(req.body.user); + const message = mid && orch.getConverters()?.get('messages').convertById(mid); + + const action = { + type, + container, + appId, + actionId, + message, + triggerId, + payload, + user, + visitor, + room, + }; + + try { + const eventInterface = !visitor ? AppInterface.IUIKitInteractionHandler : AppInterface.IUIKitLivechatInteractionHandler; + + const result = Promise.await(orch.triggerEvent(eventInterface, action)); + + res.send(result); + } catch (e) { + res.status(500).send(e); // e.message + } + break; + } + + case UIKitIncomingInteractionType.VIEW_CLOSED: { + const { + type, + actionId, + payload: { view, isCleared }, + } = req.body; + + const user = orch.getConverters()?.get('users').convertToApp(req.body.user); + + const action = { + type, + appId, + actionId, + user, + payload: { + view, + isCleared, + }, + }; + + try { + Promise.await(orch.triggerEvent('IUIKitInteractionHandler', action)); + + res.sendStatus(200); + } catch (e) { + res.status(500).send(e); // e.message + } + break; + } + + case UIKitIncomingInteractionType.VIEW_SUBMIT: { + const { type, actionId, triggerId, payload } = req.body; + + const user = orch.getConverters()?.get('users').convertToApp(req.body.user); + + const action = { + type, + appId, + actionId, + triggerId, + payload, + user, + }; + + try { + const result = Promise.await(orch.triggerEvent('IUIKitInteractionHandler', action)); + + res.send(result); + } catch (e) { + res.status(500).send(e); // e.message + } + break; + } + + case UIKitIncomingInteractionType.ACTION_BUTTON: { + const { + type, + actionId, + triggerId, + rid, + mid, + payload: { context }, + } = req.body; + + const room = orch.getConverters()?.get('rooms').convertById(rid); + const user = orch.getConverters()?.get('users').convertToApp(req.body.user); + const message = mid && orch.getConverters()?.get('messages').convertById(mid); + + const action = { + type, + appId, + actionId, + triggerId, + user, + room, + message, + payload: { + context, + }, + }; + + try { + const result = Promise.await(orch.triggerEvent('IUIKitInteractionHandler', action)); + + res.send(result); + } catch (e) { + res.status(500).send(e); // e.message + } + break; + } + + default: { + res.status(400).send({ error: 'Unknown action' }); + } + } + + // TODO: validate payloads per type + }; + +export class AppUIKitInteractionApi { + orch: AppServerOrchestrator; + + constructor(orch: AppServerOrchestrator) { + this.orch = orch; + + router.post('/:appId', appsRoutes(orch)); + } +} diff --git a/apps/meteor/app/apps/server/communication/websockets.js b/apps/meteor/app/apps/server/communication/websockets.ts similarity index 61% rename from apps/meteor/app/apps/server/communication/websockets.js rename to apps/meteor/app/apps/server/communication/websockets.ts index 005e74345733b..cfdfdf2b77b56 100644 --- a/apps/meteor/app/apps/server/communication/websockets.js +++ b/apps/meteor/app/apps/server/communication/websockets.ts @@ -1,23 +1,35 @@ -import { AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import { AppStatus, AppStatusUtils } from '@rocket.chat/apps-engine/definition/AppStatus'; +import { ISetting } from '@rocket.chat/core-typings'; +import { IStreamer } from 'meteor/rocketchat:streamer'; import { SystemLogger } from '../../../../server/lib/logger/system'; import notifications from '../../../notifications/server/lib/Notifications'; - -export const AppEvents = Object.freeze({ - APP_ADDED: 'app/added', - APP_REMOVED: 'app/removed', - APP_UPDATED: 'app/updated', - APP_STATUS_CHANGE: 'app/statusUpdate', - APP_SETTING_UPDATED: 'app/settingUpdated', - COMMAND_ADDED: 'command/added', - COMMAND_DISABLED: 'command/disabled', - COMMAND_UPDATED: 'command/updated', - COMMAND_REMOVED: 'command/removed', - ACTIONS_CHANGED: 'actions/changed', -}); +import { AppServerOrchestrator } from '../orchestrator'; + +export enum AppEvents { + APP_ADDED = 'app/added', + APP_REMOVED = 'app/removed', + APP_UPDATED = 'app/updated', + APP_STATUS_CHANGE = 'app/statusUpdate', + APP_SETTING_UPDATED = 'app/settingUpdated', + COMMAND_ADDED = 'command/added', + COMMAND_DISABLED = 'command/disabled', + COMMAND_UPDATED = 'command/updated', + COMMAND_REMOVED = 'command/removed', + ACTIONS_CHANGED = 'actions/changed', +} export class AppServerListener { - constructor(orch, engineStreamer, clientStreamer, received) { + private orch: AppServerOrchestrator; + + engineStreamer: IStreamer; + + clientStreamer: IStreamer; + + received; + + constructor(orch: AppServerOrchestrator, engineStreamer: IStreamer, clientStreamer: IStreamer, received: Map) { this.orch = orch; this.engineStreamer = engineStreamer; this.clientStreamer = clientStreamer; @@ -36,13 +48,13 @@ export class AppServerListener { this.engineStreamer.on(AppEvents.COMMAND_REMOVED, this.onCommandRemoved.bind(this)); } - async onAppAdded(appId) { - await this.orch.getManager().loadOne(appId); + async onAppAdded(appId: string): Promise { + await (this.orch.getManager()! as any).loadOne(appId); // TO-DO: fix type this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_ADDED, appId); } - async onAppStatusUpdated({ appId, status }) { - const app = this.orch.getManager().getOneById(appId); + async onAppStatusUpdated({ appId, status }: { appId: string; status: AppStatus }): Promise { + const app = this.orch.getManager()?.getOneById(appId); if (!app || app.getStatus() === status) { return; @@ -55,70 +67,81 @@ export class AppServerListener { }); if (AppStatusUtils.isEnabled(status)) { - await this.orch.getManager().enable(appId).catch(SystemLogger.error); + await this.orch.getManager()?.enable(appId).catch(SystemLogger.error); this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_STATUS_CHANGE, { appId, status }); } else if (AppStatusUtils.isDisabled(status)) { - await this.orch.getManager().disable(appId, status, true).catch(SystemLogger.error); + await this.orch.getManager()?.disable(appId, status, true).catch(SystemLogger.error); this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_STATUS_CHANGE, { appId, status }); } } - async onAppSettingUpdated({ appId, setting }) { - this.received.set(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting.id}`, { + async onAppSettingUpdated({ appId, setting }: { appId: string; setting: ISetting }): Promise { + this.received.set(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting._id}`, { appId, setting, when: new Date(), }); - await this.orch.getManager().getSettingsManager().updateAppSetting(appId, setting); + await this.orch + .getManager()! + .getSettingsManager() + .updateAppSetting(appId, setting as any); // TO-DO: fix type of `setting` this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_SETTING_UPDATED, { appId }); } - async onAppUpdated(appId) { + async onAppUpdated(appId: string): Promise { this.received.set(`${AppEvents.APP_UPDATED}_${appId}`, { appId, when: new Date() }); - const storageItem = await this.orch.getStorage().retrieveOne(appId); + const storageItem = await this.orch.getStorage()!.retrieveOne(appId); - const appPackage = await this.orch.getAppSourceStorage().fetch(storageItem); + const appPackage = await this.orch.getAppSourceStorage()!.fetch(storageItem); - await this.orch.getManager().updateLocal(storageItem, appPackage); + await this.orch.getManager()!.updateLocal(storageItem, appPackage); this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_UPDATED, appId); } - async onAppRemoved(appId) { - const app = this.orch.getManager().getOneById(appId); + async onAppRemoved(appId: string): Promise { + const app = this.orch.getManager()!.getOneById(appId); if (!app) { return; } - await this.orch.getManager().removeLocal(appId); + await this.orch.getManager()!.removeLocal(appId); this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_REMOVED, appId); } - async onCommandAdded(command) { + async onCommandAdded(command: string): Promise { this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_ADDED, command); } - async onCommandDisabled(command) { + async onCommandDisabled(command: string): Promise { this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_DISABLED, command); } - async onCommandUpdated(command) { + async onCommandUpdated(command: string): Promise { this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_UPDATED, command); } - async onCommandRemoved(command) { + async onCommandRemoved(command: string): Promise { this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_REMOVED, command); } - async onActionsChanged() { + async onActionsChanged(): Promise { this.clientStreamer.emitWithoutBroadcast(AppEvents.ACTIONS_CHANGED); } } export class AppServerNotifier { - constructor(orch) { + engineStreamer: IStreamer; + + clientStreamer: IStreamer; + + received: Map; + + listener: AppServerListener; + + constructor(orch: AppServerOrchestrator) { this.engineStreamer = notifications.streamAppsEngine; // This is used to broadcast to the web clients @@ -128,17 +151,17 @@ export class AppServerNotifier { this.listener = new AppServerListener(orch, this.engineStreamer, this.clientStreamer, this.received); } - async appAdded(appId) { + async appAdded(appId: string): Promise { this.engineStreamer.emit(AppEvents.APP_ADDED, appId); this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_ADDED, appId); } - async appRemoved(appId) { + async appRemoved(appId: string): Promise { this.engineStreamer.emit(AppEvents.APP_REMOVED, appId); this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_REMOVED, appId); } - async appUpdated(appId) { + async appUpdated(appId: string): Promise { if (this.received.has(`${AppEvents.APP_UPDATED}_${appId}`)) { this.received.delete(`${AppEvents.APP_UPDATED}_${appId}`); return; @@ -148,7 +171,7 @@ export class AppServerNotifier { this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_UPDATED, appId); } - async appStatusUpdated(appId, status) { + async appStatusUpdated(appId: string, status: AppStatus): Promise { if (this.received.has(`${AppEvents.APP_STATUS_CHANGE}_${appId}`)) { const details = this.received.get(`${AppEvents.APP_STATUS_CHANGE}_${appId}`); if (details.status === status) { @@ -161,9 +184,9 @@ export class AppServerNotifier { this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_STATUS_CHANGE, { appId, status }); } - async appSettingsChange(appId, setting) { - if (this.received.has(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting.id}`)) { - this.received.delete(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting.id}`); + async appSettingsChange(appId: string, setting: ISetting): Promise { + if (this.received.has(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting._id}`)) { + this.received.delete(`${AppEvents.APP_SETTING_UPDATED}_${appId}_${setting._id}`); return; } @@ -171,27 +194,27 @@ export class AppServerNotifier { this.clientStreamer.emitWithoutBroadcast(AppEvents.APP_SETTING_UPDATED, { appId }); } - async commandAdded(command) { + async commandAdded(command: string): Promise { this.engineStreamer.emit(AppEvents.COMMAND_ADDED, command); this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_ADDED, command); } - async commandDisabled(command) { + async commandDisabled(command: string): Promise { this.engineStreamer.emit(AppEvents.COMMAND_DISABLED, command); this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_DISABLED, command); } - async commandUpdated(command) { + async commandUpdated(command: string): Promise { this.engineStreamer.emit(AppEvents.COMMAND_UPDATED, command); this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_UPDATED, command); } - async commandRemoved(command) { + async commandRemoved(command: string): Promise { this.engineStreamer.emit(AppEvents.COMMAND_REMOVED, command); this.clientStreamer.emitWithoutBroadcast(AppEvents.COMMAND_REMOVED, command); } - async actionsChanged() { + async actionsChanged(): Promise { this.clientStreamer.emitWithoutBroadcast(AppEvents.ACTIONS_CHANGED); } } diff --git a/apps/meteor/app/authorization/client/index.js b/apps/meteor/app/authorization/client/index.ts similarity index 100% rename from apps/meteor/app/authorization/client/index.js rename to apps/meteor/app/authorization/client/index.ts diff --git a/apps/meteor/app/authorization/client/lib/streamer.js b/apps/meteor/app/authorization/client/lib/streamer.ts similarity index 100% rename from apps/meteor/app/authorization/client/lib/streamer.js rename to apps/meteor/app/authorization/client/lib/streamer.ts diff --git a/apps/meteor/app/authorization/index.js b/apps/meteor/app/authorization/index.js index a67eca871efbb..c20f7ea60706a 100644 --- a/apps/meteor/app/authorization/index.js +++ b/apps/meteor/app/authorization/index.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; if (Meteor.isClient) { - module.exports = require('./client/index.js'); + module.exports = require('./client/'); } if (Meteor.isServer) { module.exports = require('./server/index.js'); diff --git a/apps/meteor/app/authorization/server/functions/canAccessRoom.ts b/apps/meteor/app/authorization/server/functions/canAccessRoom.ts index 7b0361f7719d4..16e0a76b51e5f 100644 --- a/apps/meteor/app/authorization/server/functions/canAccessRoom.ts +++ b/apps/meteor/app/authorization/server/functions/canAccessRoom.ts @@ -8,7 +8,6 @@ export const roomAccessAttributes = { t: 1, teamId: 1, prid: 1, - tokenpass: 1, }; export const canAccessRoom = (...args: Parameters): boolean => Promise.await(canAccessRoomAsync(...args)); diff --git a/apps/meteor/app/authorization/server/functions/canDeleteMessage.js b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts similarity index 73% rename from apps/meteor/app/authorization/server/functions/canDeleteMessage.js rename to apps/meteor/app/authorization/server/functions/canDeleteMessage.ts index 1e5f2a577eeff..36d7b30394c97 100644 --- a/apps/meteor/app/authorization/server/functions/canDeleteMessage.js +++ b/apps/meteor/app/authorization/server/functions/canDeleteMessage.ts @@ -1,13 +1,15 @@ +import { IUser } from '@rocket.chat/core-typings'; + import { hasPermissionAsync } from './hasPermission'; import { getValue } from '../../../settings/server/raw'; -import { Rooms } from '../../../models'; +import { Rooms } from '../../../models/server'; -const elapsedTime = (ts) => { +const elapsedTime = (ts: number): number => { const dif = Date.now() - ts; return Math.round(dif / 1000 / 60); }; -export const canDeleteMessageAsync = async (uid, { u, rid, ts }) => { +export const canDeleteMessageAsync = async (uid: string, { u, rid, ts }: { u: IUser; rid: string; ts: number }): Promise => { const forceDelete = await hasPermissionAsync(uid, 'force-delete-message', rid); if (forceDelete) { @@ -47,4 +49,5 @@ export const canDeleteMessageAsync = async (uid, { u, rid, ts }) => { return true; }; -export const canDeleteMessage = (uid, { u, rid, ts }) => Promise.await(canDeleteMessageAsync(uid, { u, rid, ts })); +export const canDeleteMessage = (uid: string, { u, rid, ts }: { u: IUser; rid: string; ts: number }): boolean => + Promise.await(canDeleteMessageAsync(uid, { u, rid, ts })); diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomTokens.js b/apps/meteor/app/channel-settings/server/functions/saveRoomTokens.js deleted file mode 100644 index 400da0386611f..0000000000000 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomTokens.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Match } from 'meteor/check'; - -import { Rooms } from '../../../models'; - -export const saveRoomTokenpass = function (rid, tokenpass) { - if (!Match.test(rid, String)) { - throw new Meteor.Error('invalid-room', 'Invalid room', { - function: 'RocketChat.saveRoomTokens', - }); - } - - return Rooms.setTokenpassById(rid, tokenpass); -}; diff --git a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.js b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.js index 3462c720a5700..0027ba0856837 100644 --- a/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.js +++ b/apps/meteor/app/channel-settings/server/methods/saveRoomSettings.js @@ -1,5 +1,5 @@ import { Meteor } from 'meteor/meteor'; -import { Match, check } from 'meteor/check'; +import { Match } from 'meteor/check'; import { TEAM_TYPE } from '@rocket.chat/core-typings'; import { setRoomAvatar } from '../../../lib/server/functions/setRoomAvatar'; @@ -15,7 +15,6 @@ import { saveRoomType } from '../functions/saveRoomType'; import { saveRoomReadOnly } from '../functions/saveRoomReadOnly'; import { saveReactWhenReadOnly } from '../functions/saveReactWhenReadOnly'; import { saveRoomSystemMessages } from '../functions/saveRoomSystemMessages'; -import { saveRoomTokenpass } from '../functions/saveRoomTokens'; import { saveRoomEncrypted } from '../functions/saveRoomEncrypted'; import { saveStreamingOptions } from '../functions/saveStreamingOptions'; import { Team } from '../../../../server/sdk'; @@ -36,7 +35,6 @@ const fields = [ 'systemMessages', 'default', 'joinCode', - 'tokenpass', 'streamingOptions', 'retentionEnabled', 'retentionMaxAge', @@ -195,18 +193,6 @@ const settingSavers = { Team.update(user._id, room.teamId, { type, updateRoom: false }); } }, - tokenpass({ value, rid }) { - check(value, { - require: String, - tokens: [ - { - token: String, - balance: String, - }, - ], - }); - saveRoomTokenpass(rid, value); - }, streamingOptions({ value, rid }) { saveStreamingOptions(rid, value); }, diff --git a/apps/meteor/app/importer-csv/server/importer.js b/apps/meteor/app/importer-csv/server/importer.js index d4d3d014dbfae..1d2ce78e38dd7 100644 --- a/apps/meteor/app/importer-csv/server/importer.js +++ b/apps/meteor/app/importer-csv/server/importer.js @@ -7,7 +7,9 @@ export class CsvImporter extends Base { constructor(info, importRecord) { super(info, importRecord); - this.csvParser = require('csv-parse/lib/sync'); + const { parse } = require('csv-parse/lib/sync'); + + this.csvParser = parse; } prepareUsingLocalFile(fullFilePath) { diff --git a/apps/meteor/app/importer-slack-users/server/importer.js b/apps/meteor/app/importer-slack-users/server/importer.js index 5fcf3924499b8..9646ac79e7b34 100644 --- a/apps/meteor/app/importer-slack-users/server/importer.js +++ b/apps/meteor/app/importer-slack-users/server/importer.js @@ -11,7 +11,9 @@ export class SlackUsersImporter extends Base { constructor(info, importRecord) { super(info, importRecord); - this.csvParser = require('csv-parse/lib/sync'); + const { parse } = require('csv-parse/lib/sync'); + + this.csvParser = parse; this.userMap = new Map(); this.admins = []; // Array of ids of the users which are admins } diff --git a/apps/meteor/app/lib/server/functions/deleteUser.ts b/apps/meteor/app/lib/server/functions/deleteUser.ts index 63339692bb5d8..056b3750a2d5a 100644 --- a/apps/meteor/app/lib/server/functions/deleteUser.ts +++ b/apps/meteor/app/lib/server/functions/deleteUser.ts @@ -75,7 +75,7 @@ export async function deleteUser(userId: string, confirmRelinquish = false): Pro } // removes user's avatar - if (user.avatarOrigin === 'upload' || user.avatarOrigin === 'url') { + if (user.avatarOrigin === 'upload' || user.avatarOrigin === 'url' || user.avatarOrigin === 'rest') { FileUpload.getStore('Avatars').deleteByName(user.username); } diff --git a/apps/meteor/app/lib/server/functions/getFullUserData.js b/apps/meteor/app/lib/server/functions/getFullUserData.ts similarity index 65% rename from apps/meteor/app/lib/server/functions/getFullUserData.js rename to apps/meteor/app/lib/server/functions/getFullUserData.ts index d328f3b058324..74bf410aee9be 100644 --- a/apps/meteor/app/lib/server/functions/getFullUserData.js +++ b/apps/meteor/app/lib/server/functions/getFullUserData.ts @@ -1,7 +1,9 @@ -import { Logger } from '../../../logger'; +import { IUser } from '@rocket.chat/core-typings'; + +import { Logger } from '../../../logger/server'; import { settings } from '../../../settings/server'; import { Users } from '../../../models/server'; -import { hasPermission } from '../../../authorization'; +import { hasPermission } from '../../../authorization/server'; const logger = new Logger('getFullUserData'); @@ -18,7 +20,7 @@ const defaultFields = { statusText: 1, avatarETag: 1, extension: 1, -}; +} as const; const fullFields = { emails: 1, @@ -31,12 +33,12 @@ const fullFields = { requirePasswordChange: 1, requirePasswordChangeReason: 1, roles: 1, -}; +} as const; -let publicCustomFields = {}; -let customFields = {}; +let publicCustomFields: Record = {}; +let customFields: Record = {}; -settings.watch('Accounts_CustomFields', (value) => { +settings.watch('Accounts_CustomFields', (value) => { publicCustomFields = {}; customFields = {}; @@ -58,29 +60,23 @@ settings.watch('Accounts_CustomFields', (value) => { } }); -const getCustomFields = (canViewAllInfo) => (canViewAllInfo ? customFields : publicCustomFields); +const getCustomFields = (canViewAllInfo: boolean): Record => (canViewAllInfo ? customFields : publicCustomFields); -const getFields = (canViewAllInfo) => ({ +const getFields = (canViewAllInfo: boolean): Record => ({ ...defaultFields, ...(canViewAllInfo && fullFields), ...getCustomFields(canViewAllInfo), }); -const removePasswordInfo = (user) => { - if (user && user.services) { - delete user.services.password; - delete user.services.email; - delete user.services.resume; - delete user.services.emailCode; - delete user.services.cloud; - delete user.services.email2fa; - delete user.services.totp; - } - - return user; +const removePasswordInfo = (user: IUser): Omit => { + const { services, ...result } = user; + return result; }; -export function getFullUserDataByIdOrUsername({ userId, filterId, filterUsername }) { +export async function getFullUserDataByIdOrUsername( + userId: string, + { filterId, filterUsername }: { filterId: string; filterUsername?: undefined } | { filterId?: undefined; filterUsername: string }, +): Promise { const caller = Users.findOneById(userId, { fields: { username: 1 } }); const targetUser = filterId || filterUsername; const myself = (filterId && targetUser === userId) || (filterUsername && targetUser === caller.username); diff --git a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts index a5d6313b1be31..7ba891c38a357 100644 --- a/apps/meteor/app/lib/server/functions/saveUserIdentity.ts +++ b/apps/meteor/app/lib/server/functions/saveUserIdentity.ts @@ -70,9 +70,13 @@ export function saveUserIdentity({ LivechatDepartmentAgents.replaceUsernameOfAgentByUserId(user._id, username); const fileStore = FileUpload.getStore('Avatars'); - const file = fileStore.model.findOneByName(previousUsername); + const previousFile = Promise.await(fileStore.model.findOneByName(previousUsername)); + const file = Promise.await(fileStore.model.findOneByName(username)); if (file) { - fileStore.model.updateFileNameById(file._id, username); + fileStore.model.deleteFile(file._id); + } + if (previousFile) { + fileStore.model.updateFileNameById(previousFile._id, username); } } diff --git a/apps/meteor/app/lib/server/functions/setUserAvatar.ts b/apps/meteor/app/lib/server/functions/setUserAvatar.ts index e72b847c5cee2..4718303b21caa 100644 --- a/apps/meteor/app/lib/server/functions/setUserAvatar.ts +++ b/apps/meteor/app/lib/server/functions/setUserAvatar.ts @@ -8,12 +8,26 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import { api } from '../../../../server/sdk/api'; import { fetch } from '../../../../server/lib/http/fetch'; -export const setUserAvatar = function ( +export function setUserAvatar( + user: Pick, + dataURI: Buffer, + contentType: string, + service: 'rest', + etag?: string, +): void; +export function setUserAvatar( user: Pick, dataURI: string, contentType: string, service: 'initials' | 'url' | 'rest' | string, etag?: string, +): void; +export function setUserAvatar( + user: Pick, + dataURI: string | Buffer, + contentType: string, + service: 'initials' | 'url' | 'rest' | string, + etag?: string, ): void { if (service === 'initials') { Users.setAvatarData(user._id, service, null); @@ -22,7 +36,7 @@ export const setUserAvatar = function ( const { buffer, type } = Promise.await( (async (): Promise<{ buffer: Buffer; type: string }> => { - if (service === 'url') { + if (service === 'url' && typeof dataURI === 'string') { let response: Response; try { response = await fetch(dataURI); @@ -69,7 +83,7 @@ export const setUserAvatar = function ( if (service === 'rest') { return { - buffer: Buffer.from(dataURI, 'binary'), + buffer: dataURI instanceof Buffer ? dataURI : Buffer.from(dataURI, 'binary'), type: contentType, }; } @@ -103,4 +117,4 @@ export const setUserAvatar = function ( avatarETag, }); }, 500); -}; +} diff --git a/apps/meteor/app/lib/server/methods/createPrivateGroup.js b/apps/meteor/app/lib/server/methods/createPrivateGroup.js index c274cca5840d7..a9b4f62406f1a 100644 --- a/apps/meteor/app/lib/server/methods/createPrivateGroup.js +++ b/apps/meteor/app/lib/server/methods/createPrivateGroup.js @@ -19,22 +19,6 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'createPrivateGroup' }); } - // validate extra data schema - check( - extraData, - Match.ObjectIncluding({ - tokenpass: Match.Maybe({ - require: String, - tokens: [ - { - token: String, - balance: String, - }, - ], - }), - }), - ); - return createRoom('p', name, Meteor.user() && Meteor.user().username, members, readOnly, { customFields, ...extraData, diff --git a/apps/meteor/app/lib/server/methods/joinRoom.ts b/apps/meteor/app/lib/server/methods/joinRoom.ts index fad1bb88dea0d..dcde906be1afc 100644 --- a/apps/meteor/app/lib/server/methods/joinRoom.ts +++ b/apps/meteor/app/lib/server/methods/joinRoom.ts @@ -3,7 +3,6 @@ import { check } from 'meteor/check'; import { hasPermission, canAccessRoom } from '../../../authorization/server'; import { Rooms } from '../../../models/server'; -import { Tokenpass, updateUserTokenpassBalances } from '../../../tokenpass/server'; import { addUserToRoom } from '../functions'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { RoomMemberActions } from '../../../../definition/IRoomTypeConfig'; @@ -28,23 +27,13 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'joinRoom' }); } - // TODO we should have a 'beforeJoinRoom' call back so external services can do their own validations - - if (room.tokenpass && user && user.services && user.services.tokenpass) { - const balances = updateUserTokenpassBalances(user); - - if (!Tokenpass.validateAccess(room.tokenpass, balances)) { - throw new Meteor.Error('error-not-allowed', 'Token required', { method: 'joinRoom' }); - } - } else { - if (!canAccessRoom(room, user)) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'joinRoom' }); - } - if (room.joinCodeRequired === true && code !== room.joinCode && !hasPermission(user._id, 'join-without-join-code')) { - throw new Meteor.Error('error-code-invalid', 'Invalid Room Password', { - method: 'joinRoom', - }); - } + if (!canAccessRoom(room, user)) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'joinRoom' }); + } + if (room.joinCodeRequired === true && code !== room.joinCode && !hasPermission(user._id, 'join-without-join-code')) { + throw new Meteor.Error('error-code-invalid', 'Invalid Room Password', { + method: 'joinRoom', + }); } return addUserToRoom(rid, user); diff --git a/apps/meteor/app/livechat/imports/server/rest/upload.js b/apps/meteor/app/livechat/imports/server/rest/upload.js index 8547b94645f1d..f6d4f1485a324 100644 --- a/apps/meteor/app/livechat/imports/server/rest/upload.js +++ b/apps/meteor/app/livechat/imports/server/rest/upload.js @@ -36,10 +36,13 @@ API.v1.addRoute('livechat/upload/:rid', { return API.v1.unauthorized(); } - const { file, ...fields } = Promise.await( - getUploadFormData({ - request: this.request, - }), + const [file, fields] = Promise.await( + getUploadFormData( + { + request: this.request, + }, + { field: 'file' }, + ), ); if (!fileUploadIsValidContentType(file.mimetype)) { diff --git a/apps/meteor/app/models/server/models/Rooms.js b/apps/meteor/app/models/server/models/Rooms.js index 593fdab626da8..e86f9fa054c01 100644 --- a/apps/meteor/app/models/server/models/Rooms.js +++ b/apps/meteor/app/models/server/models/Rooms.js @@ -17,9 +17,6 @@ export class Rooms extends Base { this.tryEnsureIndex({ t: 1 }); this.tryEnsureIndex({ 'u._id': 1 }); this.tryEnsureIndex({ ts: 1 }); - // Tokenpass - this.tryEnsureIndex({ 'tokenpass.tokens.token': 1 }, { sparse: true }); - this.tryEnsureIndex({ tokenpass: 1 }, { sparse: true }); // discussions this.tryEnsureIndex({ prid: 1 }, { sparse: true }); this.tryEnsureIndex({ fname: 1 }, { sparse: true }); @@ -95,38 +92,6 @@ export class Rooms extends Base { return this.update(query, update); } - findByTokenpass(tokens) { - const query = { - 'tokenpass.tokens.token': { - $in: tokens, - }, - }; - - return this._db.find(query).fetch(); - } - - setTokensById(_id, tokens) { - const update = { - $set: { - 'tokenpass.tokens.token': tokens, - }, - }; - - return this.update({ _id }, update); - } - - findAllTokenChannels() { - const query = { - tokenpass: { $exists: true }, - }; - const options = { - fields: { - tokenpass: 1, - }, - }; - return this._db.find(query, options); - } - setReactionsInLastMessage(roomId, lastMessage) { return this.update({ _id: roomId }, { $set: { 'lastMessage.reactions': lastMessage.reactions } }); } @@ -242,16 +207,6 @@ export class Rooms extends Base { return this.update({ _id }, update); } - setTokenpassById(_id, tokenpass) { - const update = { - $set: { - tokenpass, - }, - }; - - return this.update({ _id }, update); - } - setReadOnlyById(_id, readOnly) { const query = { _id, diff --git a/apps/meteor/app/models/server/models/Users.js b/apps/meteor/app/models/server/models/Users.js index 3e8eb705d3d38..dd42ac05b0555 100644 --- a/apps/meteor/app/models/server/models/Users.js +++ b/apps/meteor/app/models/server/models/Users.js @@ -338,30 +338,6 @@ export class Users extends Base { return this.findOne(query, options); } - setTokenpassTcaBalances(_id, tcaBalances) { - const update = { - $set: { - 'services.tokenpass.tcaBalances': tcaBalances, - }, - }; - - return this.update(_id, update); - } - - getTokenBalancesByUserId(userId) { - const query = { - _id: userId, - }; - - const options = { - fields: { - 'services.tokenpass.tcaBalances': 1, - }, - }; - - return this.findOne(query, options); - } - roleBaseQuery(userId) { return { _id: userId }; } diff --git a/apps/meteor/app/models/server/raw/Users.js b/apps/meteor/app/models/server/raw/Users.js index da01f11cbd3c8..88c36e118aee5 100644 --- a/apps/meteor/app/models/server/raw/Users.js +++ b/apps/meteor/app/models/server/raw/Users.js @@ -147,6 +147,12 @@ export class UsersRaw extends BaseRaw { return this.find(query, options); } + findActive(query, options = {}) { + Object.assign(query, { active: true }); + + return this.find(query, options); + } + findActiveByIds(userIds, options = {}) { const query = { _id: { $in: userIds }, diff --git a/apps/meteor/app/settings/server/SettingsRegistry.ts b/apps/meteor/app/settings/server/SettingsRegistry.ts index d6799596ed9a8..0c5c00ff2c102 100644 --- a/apps/meteor/app/settings/server/SettingsRegistry.ts +++ b/apps/meteor/app/settings/server/SettingsRegistry.ts @@ -135,8 +135,10 @@ export class SettingsRegistry { throw new Error(`Enterprise setting ${_id} is missing the invalidValue option`); } + const settingFromCodeOverwritten = overwriteSetting(settingFromCode); + const settingStored = this.store.getSetting(_id); - const settingOverwritten = overwriteSetting(settingFromCode); + const settingStoredOverwritten = settingStored && overwriteSetting(settingStored); try { validateSetting(settingFromCode._id, settingFromCode.type, settingFromCode.value); @@ -144,14 +146,14 @@ export class SettingsRegistry { IS_DEVELOPMENT && SystemLogger.error(`Invalid setting code ${_id}: ${(e as Error).message}`); } - const isOverwritten = settingFromCode !== settingOverwritten; + const isOverwritten = settingFromCode !== settingFromCodeOverwritten || (settingStored && settingStored !== settingStoredOverwritten); - const { _id: _, ...settingProps } = settingOverwritten; + const { _id: _, ...settingProps } = settingFromCodeOverwritten; - if (settingStored && !compareSettings(settingStored, settingOverwritten)) { - const { value: _value, ...settingOverwrittenProps } = settingOverwritten; + if (settingStored && !compareSettings(settingStored, settingFromCodeOverwritten)) { + const { value: _value, ...settingOverwrittenProps } = settingFromCodeOverwritten; - const overwrittenKeys = Object.keys(settingOverwritten); + const overwrittenKeys = Object.keys(settingFromCodeOverwritten); const removedKeys = Object.keys(settingStored).filter((key) => !['_updatedAt'].includes(key) && !overwrittenKeys.includes(key)); this.model.upsert( @@ -168,7 +170,7 @@ export class SettingsRegistry { } if (settingStored && isOverwritten) { - if (settingStored.value !== settingOverwritten.value) { + if (settingStored.value !== settingFromCodeOverwritten.value) { this.model.upsert({ _id }, settingProps); } return; @@ -185,7 +187,7 @@ export class SettingsRegistry { const settingOverwrittenDefault = overrideSetting(settingFromCode); - const setting = isOverwritten ? settingOverwritten : settingOverwrittenDefault; + const setting = isOverwritten ? settingFromCodeOverwritten : settingOverwrittenDefault; this.model.insert(setting); // no need to emit unless we remove the oplog diff --git a/apps/meteor/app/tokenpass/README.md b/apps/meteor/app/tokenpass/README.md deleted file mode 100644 index 8eb3a0c7a37ab..0000000000000 --- a/apps/meteor/app/tokenpass/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Tokenpass OAuth Flow - -An implementation of the Tokenpass OAuth flow with Tokenpass using RocketChat CustomOAuth. See the [Tokenpass API Reference](http://apidocs.tokenly.com/tokenpass/#oauth-integration) for more details. diff --git a/apps/meteor/app/tokenpass/client/channelSettings.css b/apps/meteor/app/tokenpass/client/channelSettings.css deleted file mode 100644 index 08c0275b14c4f..0000000000000 --- a/apps/meteor/app/tokenpass/client/channelSettings.css +++ /dev/null @@ -1,47 +0,0 @@ -.channelSettings-tokenpass { - flex-direction: column; - - &.tokenpass__editing { - & .button.edit { - visibility: hidden !important; - } - } - - & .details { - padding-left: 1rem; - } - - & button { - margin: 0 5px 0 0 !important; - } - - & button[disabled] { - opacity: 0.5; - } - - & button[disabled]:hover { - opacity: 0.5; - } - - & .js-edit { - float: right; - } - - & .chip-container { - margin: 0; - - & > li { - padding: 2px 8px; - } - } - - &__form .button { - visibility: visible !important; - } - - &__input { - height: auto !important; - margin: 0 0 10px !important; - padding: 10px !important; - } -} diff --git a/apps/meteor/app/tokenpass/client/index.js b/apps/meteor/app/tokenpass/client/index.js index e51844feeee84..e44dbe195eff9 100644 --- a/apps/meteor/app/tokenpass/client/index.js +++ b/apps/meteor/app/tokenpass/client/index.js @@ -1,9 +1 @@ import '../lib/common'; -import './startup'; -import './tokenChannelsList.html'; -import './tokenChannelsList'; -import './tokenpassChannelSettings.html'; -import './tokenpassChannelSettings'; -import './login-button.css'; -import './channelSettings.css'; -import './styles.css'; diff --git a/apps/meteor/app/tokenpass/client/login-button.css b/apps/meteor/app/tokenpass/client/login-button.css deleted file mode 100644 index 1171df2633ccd..0000000000000 --- a/apps/meteor/app/tokenpass/client/login-button.css +++ /dev/null @@ -1,14 +0,0 @@ -.icon-tokenpass.service-icon { - display: inline-block; - - width: 25px; - height: 1em; - - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAQCAYAAAAbBi9cAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAOxAAADsQBlSsOGwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAAddEVYdFRpdGxlAFRva2VubHlfTG9nb19JY29uX1doaXRldF224QAAAapJREFUOI2lk89OE1EUxn9nrumS2GZIhiaFBFekQsILsME/JC7AF2DVpT4BBI3GNRvZoG58Ad0SdCEvwKJW3ehCQmfajB1YAr33uJiZOg2hbeK3u1/O/d1zvnuvUFCr1SqVK/4h6EruqdAAVJR3/yrlKOnF9+v1+mXueEVQpeI/LUJulq6Uy/6TojMAhWE4raLb4yF5U/qs2+0G10Aq8hLl9sQgmOo793wIFEXRIkoj834C8QhAnNWA0jjpdJYAbgGosgsY4EqdfYQxDwTuAmDlBwBG99O9fNW+OfCMbQIlz+kusCrtdmcD0Q/ZaSeoe2iMuRg1k7W2hHgHwGzamTyWdhh9AxbGRzJS37202/+WymkUrYvyMTN+o25t3GiXIsZYdwjMpRQ2BCAMo08Kq6RhLxbDVivvAcToJlwPW+DzzExwTyC9fqcck97cL2AK8LPTGgCivM0aioFz4A5gPWE5CIKmBxAEQRP0TVY4n0NukJ9BAN1P9xZetu33t4DeqGyGJJyhupMvB6BardZD5dXEIMeLarU6+AFDvz9J4tcgRxNgviTJn72i8RdLE6KbF5O00AAAAABJRU5ErkJggg==); - background-repeat: no-repeat; - background-position: center center; -} - -.button.external-login.tokenpass { - background-color: #4170a0; -} diff --git a/apps/meteor/app/tokenpass/client/startup.js b/apps/meteor/app/tokenpass/client/startup.js deleted file mode 100644 index 164e2cc50b872..0000000000000 --- a/apps/meteor/app/tokenpass/client/startup.js +++ /dev/null @@ -1,21 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { ChannelSettings } from '../../channel-settings'; -import { Rooms } from '../../models'; - -Meteor.startup(function () { - ChannelSettings.addOption({ - group: ['room'], - id: 'tokenpass', - template: 'channelSettings__tokenpass', - validation(data) { - if (data && data.rid) { - const room = Rooms.findOne(data.rid, { fields: { tokenpass: 1 } }); - - return room && room.tokenpass; - } - - return false; - }, - }); -}); diff --git a/apps/meteor/app/tokenpass/client/styles.css b/apps/meteor/app/tokenpass/client/styles.css deleted file mode 100644 index 126731991bd74..0000000000000 --- a/apps/meteor/app/tokenpass/client/styles.css +++ /dev/null @@ -1,12 +0,0 @@ -.icon-tokenpass { - display: inline-block; - - width: 24px; - height: 0.8em; - - margin-bottom: -1px; - - background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MzIgMzgxLjUiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDojNDE3MGEwO308L3N0eWxlPjwvZGVmcz48dGl0bGU+VG9rZW5seV9JY29uPC90aXRsZT48ZyBpZD0iTGF5ZXJfMiIgZGF0YS1uYW1lPSJMYXllciAyIj48ZyBpZD0iTGF5ZXJfMS0yIiBkYXRhLW5hbWU9IkxheWVyIDEiPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTQyOC4zMSwxNzcsMzM0LjA3LDEzLjc2QTI3LjUyLDI3LjUyLDAsMCwwLDMxMC4yNCwwaC03OS40VjEzMy44OWg3NFYxMTIuODRoMTcuNzJsMTguMTMsMzEuNGExMi41MiwxMi41MiwwLDAsMSwwLDEyLjcxLDEyLjc1LDEyLjc1LDAsMCwxLTExLjIyLDYuNTRIMTAyLjU2YTEyLjc1LDEyLjc1LDAsMCwxLTExLjIyLTYuNTQsMTIuNTIsMTIuNTIsMCwwLDEsMC0xMi43MWwxOC4xMy0zMS40aDE3LjcydjIxLjA1aDc0VjBoLTc5LjRBMjcuNTIsMjcuNTIsMCwwLDAsOTcuOTMsMTMuNzZMMy42OSwxNzdhMjcuNTIsMjcuNTIsMCwwLDAsMCwyNy41Mkw5Ny45MywzNjcuNzRhMjcuNTIsMjcuNTIsMCwwLDAsMjMuODQsMTMuNzZoNzkuNFYyNDcuNjFoLTc0djIxLjA2SDEwOS40N2wtMTguMTMtMzEuNGExMi41MiwxMi41MiwwLDAsMSwwLTEyLjcxQTEyLjc1LDEyLjc1LDAsMCwxLDEwMi41NiwyMThIMzI5LjQ0YTEyLjc1LDEyLjc1LDAsMCwxLDExLjIyLDYuNTQsMTIuNTIsMTIuNTIsMCwwLDEsMCwxMi43MWwtMTguMTMsMzEuNEgzMDQuODFWMjQ3LjYxaC03NFYzODEuNWg3OS40YTI3LjUyLDI3LjUyLDAsMCwwLDIzLjg0LTEzLjc2bDk0LjI0LTE2My4yM0EyNy41MiwyNy41MiwwLDAsMCw0MjguMzEsMTc3WiIvPjwvZz48L2c+PC9zdmc+); - background-repeat: no-repeat; - background-position: center center; -} diff --git a/apps/meteor/app/tokenpass/client/tokenChannelsList.html b/apps/meteor/app/tokenpass/client/tokenChannelsList.html deleted file mode 100644 index ae892bfd614a8..0000000000000 --- a/apps/meteor/app/tokenpass/client/tokenChannelsList.html +++ /dev/null @@ -1,14 +0,0 @@ - diff --git a/apps/meteor/app/tokenpass/client/tokenChannelsList.js b/apps/meteor/app/tokenpass/client/tokenChannelsList.js deleted file mode 100644 index 032ec605985d9..0000000000000 --- a/apps/meteor/app/tokenpass/client/tokenChannelsList.js +++ /dev/null @@ -1,33 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { Tracker } from 'meteor/tracker'; -import { Template } from 'meteor/templating'; - -import { Subscriptions } from '../../models'; - -Template.tokenChannelsList.helpers({ - rooms() { - return Template.instance() - .tokenpassRooms.get() - .filter((room) => Subscriptions.find({ rid: room._id }).count() === 0); - }, -}); - -Template.tokenChannelsList.onRendered(function () { - Tracker.autorun((c) => { - const user = Meteor.user(); - if (user && user.services && user.services.tokenpass) { - c.stop(); - - Meteor.call('findTokenChannels', (error, result) => { - if (!error) { - this.tokenpassRooms.set(result); - } - }); - } - }); -}); - -Template.tokenChannelsList.onCreated(function () { - this.tokenpassRooms = new ReactiveVar([]); -}); diff --git a/apps/meteor/app/tokenpass/client/tokenpassChannelSettings.html b/apps/meteor/app/tokenpass/client/tokenpassChannelSettings.html deleted file mode 100644 index 2ecda3b6d6262..0000000000000 --- a/apps/meteor/app/tokenpass/client/tokenpassChannelSettings.html +++ /dev/null @@ -1,39 +0,0 @@ - diff --git a/apps/meteor/app/tokenpass/client/tokenpassChannelSettings.js b/apps/meteor/app/tokenpass/client/tokenpassChannelSettings.js deleted file mode 100644 index c92260c88a048..0000000000000 --- a/apps/meteor/app/tokenpass/client/tokenpassChannelSettings.js +++ /dev/null @@ -1,119 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { ReactiveVar } from 'meteor/reactive-var'; -import { Template } from 'meteor/templating'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; - -import { t } from '../../utils'; -import { ChatRoom } from '../../models'; -import { handleError } from '../../../client/lib/utils/handleError'; -import { dispatchToastMessage } from '../../../client/lib/toast'; - -Template.channelSettings__tokenpass.helpers({ - addDisabled() { - const { balance, token } = Template.instance(); - return balance.get() && token.get() ? '' : 'disabled'; - }, - list() { - return Template.instance().list.get(); - }, - save() { - const { list, initial } = Template.instance(); - return JSON.stringify(list.get()) !== JSON.stringify(initial); - }, - editing() { - return Template.instance().editing.get() ? 'tokenpass__editing' : ''; - }, - requiredChecked() { - return Template.instance().requireAll.get() ? 'checked' : ''; - }, - requiredLabel() { - return Template.instance().requireAll.get() ? t('Require_all_tokens') : t('Require_any_token'); - }, - requiredDisabled() { - return !Template.instance().editing.get() ? 'disabled' : ''; - }, - editDisabled() { - return Template.instance().editing.get() ? 'disabled' : ''; - }, -}); - -Template.channelSettings__tokenpass.onCreated(function () { - const room = ChatRoom.findOne(this.data.rid, { fields: { tokenpass: 1 } }); - - this.editing = new ReactiveVar(false); - this.initial = room.tokenpass; - this.requireAll = new ReactiveVar(room.tokenpass.require === 'all'); - this.list = new ReactiveVar(this.initial.tokens); - this.token = new ReactiveVar(''); - this.balance = new ReactiveVar(''); -}); - -Template.channelSettings__tokenpass.events({ - 'click .js-edit'(e, i) { - i.editing.set(true); - }, - 'input [name=token]'(e, i) { - i.token.set(e.target.value); - }, - 'input [name=balance]'(e, i) { - i.balance.set(e.target.value); - }, - 'click .js-add'(e, i) { - e.preventDefault(); - const instance = Template.instance(); - const { balance, token, list } = instance; - list.set([...list.get().filter((t) => t.token !== token), { token: token.get(), balance: balance.get() }]); - - [...i.findAll('input')].forEach((el) => { - el.value = ''; - }); - return balance.set('') && token.set(''); - }, - 'click .js-remove'(e, instance) { - e.preventDefault(); - const { list, editing } = instance; - - if (!editing.get()) { - return; - } - list.set(list.get().filter((t) => t.token !== this.token)); - }, - 'click .js-save'(e, i) { - e.preventDefault(); - - const tokenpass = { - require: i.find('[name=requireAllTokens]').checked ? 'all' : 'any', - tokens: i.list.get(), - }; - - Meteor.call('saveRoomSettings', this.rid, 'tokenpass', tokenpass, function (err) { - if (err) { - return handleError(err); - } - i.editing.set(false); - i.token.set(''); - i.balance.set(''); - i.initial = tokenpass; - [...i.findAll('input')].forEach((el) => { - el.value = ''; - }); - return dispatchToastMessage({ - type: 'success', - message: TAPi18n.__('Room_tokenpass_config_changed_successfully'), - }); - }); - }, - 'click .js-cancel'(e, i) { - e.preventDefault(); - i.editing.set(false); - i.list.set(i.initial.tokens); - i.token.set(''); - i.balance.set(''); - [...i.findAll('input')].forEach((el) => { - el.value = ''; - }); - }, - 'change [name=requireAllTokens]'(e, instance) { - instance.requireAll.set(e.currentTarget.checked); - }, -}); diff --git a/apps/meteor/app/tokenpass/lib/common.js b/apps/meteor/app/tokenpass/lib/common.js index 0cd6868a3672c..46e9f0151e7d3 100644 --- a/apps/meteor/app/tokenpass/lib/common.js +++ b/apps/meteor/app/tokenpass/lib/common.js @@ -9,7 +9,7 @@ const config = { identityPath: '/oauth/user', authorizePath: '/oauth/authorize', tokenPath: '/oauth/access-token', - scope: 'user,tca,private-balances', + scope: 'user', tokenSentVia: 'payload', usernameField: 'username', mergeUsers: true, diff --git a/apps/meteor/app/tokenpass/server/Tokenpass.js b/apps/meteor/app/tokenpass/server/Tokenpass.js deleted file mode 100644 index 53707063167ae..0000000000000 --- a/apps/meteor/app/tokenpass/server/Tokenpass.js +++ /dev/null @@ -1,8 +0,0 @@ -export const Tokenpass = { - validateAccess(tokenpass, balances) { - const compFunc = tokenpass.require === 'any' ? 'some' : 'every'; - return tokenpass.tokens[compFunc]((config) => - balances.some((userToken) => config.token === userToken.asset && parseFloat(config.balance) <= parseFloat(userToken.balance)), - ); - }, -}; diff --git a/apps/meteor/app/tokenpass/server/cronRemoveUsers.js b/apps/meteor/app/tokenpass/server/cronRemoveUsers.js deleted file mode 100644 index 2376fc7824344..0000000000000 --- a/apps/meteor/app/tokenpass/server/cronRemoveUsers.js +++ /dev/null @@ -1,52 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { SyncedCron } from 'meteor/littledata:synced-cron'; - -import { updateUserTokenpassBalances } from './functions/updateUserTokenpassBalances'; -import { Tokenpass } from './Tokenpass'; -import { Rooms, Subscriptions, Users } from '../../models'; -import { removeUserFromRoom } from '../../lib/server/functions/removeUserFromRoom'; - -function removeUsersFromTokenChannels() { - const rooms = {}; - - Rooms.findAllTokenChannels().forEach((room) => { - rooms[room._id] = room.tokenpass; - }); - - const users = {}; - - Subscriptions.findByRoomIds(Object.keys(rooms)).forEach((sub) => { - if (!users[sub.u._id]) { - users[sub.u._id] = []; - } - users[sub.u._id].push(sub.rid); - }); - - Object.keys(users).forEach((user) => { - const userInfo = Users.findOneById(user); - - if (userInfo && userInfo.services && userInfo.services.tokenpass) { - const balances = updateUserTokenpassBalances(userInfo); - - users[user].forEach((roomId) => { - const valid = Tokenpass.validateAccess(rooms[roomId], balances); - - if (!valid) { - Promise.await(removeUserFromRoom(roomId, userInfo)); - } - }); - } - }); -} - -Meteor.startup(function () { - Meteor.defer(function () { - removeUsersFromTokenChannels(); - - SyncedCron.add({ - name: 'Remove users from Token Channels', - schedule: (parser) => parser.cron('0 * * * *'), - job: removeUsersFromTokenChannels, - }); - }); -}); diff --git a/apps/meteor/app/tokenpass/server/functions/getProtectedTokenpassBalances.js b/apps/meteor/app/tokenpass/server/functions/getProtectedTokenpassBalances.js deleted file mode 100644 index c6059608697bc..0000000000000 --- a/apps/meteor/app/tokenpass/server/functions/getProtectedTokenpassBalances.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { HTTP } from 'meteor/http'; - -import { settings } from '../../../settings'; - -let userAgent = 'Meteor'; -if (Meteor.release) { - userAgent += `/${Meteor.release}`; -} - -export const getProtectedTokenpassBalances = function (accessToken) { - try { - return HTTP.get(`${settings.get('API_Tokenpass_URL')}/api/v1/tca/protected/balances`, { - headers: { - 'Accept': 'application/json', - 'User-Agent': userAgent, - }, - params: { - oauth_token: accessToken, - }, - }).data; - } catch (error) { - throw new Error(`Failed to fetch protected tokenpass balances from Tokenpass. ${error.message}`); - } -}; diff --git a/apps/meteor/app/tokenpass/server/functions/getPublicTokenpassBalances.js b/apps/meteor/app/tokenpass/server/functions/getPublicTokenpassBalances.js deleted file mode 100644 index ead13ad26f838..0000000000000 --- a/apps/meteor/app/tokenpass/server/functions/getPublicTokenpassBalances.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { HTTP } from 'meteor/http'; - -import { settings } from '../../../settings'; - -let userAgent = 'Meteor'; -if (Meteor.release) { - userAgent += `/${Meteor.release}`; -} - -export const getPublicTokenpassBalances = function (accessToken) { - try { - return HTTP.get(`${settings.get('API_Tokenpass_URL')}/api/v1/tca/public/balances`, { - headers: { - 'Accept': 'application/json', - 'User-Agent': userAgent, - }, - params: { - oauth_token: accessToken, - }, - }).data; - } catch (error) { - throw new Error(`Failed to fetch public tokenpass balances from Tokenpass. ${error.message}`); - } -}; diff --git a/apps/meteor/app/tokenpass/server/functions/saveRoomTokensMinimumBalance.js b/apps/meteor/app/tokenpass/server/functions/saveRoomTokensMinimumBalance.js deleted file mode 100644 index 5361c58856bae..0000000000000 --- a/apps/meteor/app/tokenpass/server/functions/saveRoomTokensMinimumBalance.js +++ /dev/null @@ -1,17 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { Match } from 'meteor/check'; -import { escapeHTML } from '@rocket.chat/string-helpers'; - -import { Rooms } from '../../../models'; - -export const saveRoomTokensMinimumBalance = function (rid, roomTokensMinimumBalance) { - if (!Match.test(rid, String)) { - throw new Meteor.Error('invalid-room', 'Invalid room', { - function: 'RocketChat.saveRoomTokensMinimumBalance', - }); - } - - const minimumTokenBalance = parseFloat(escapeHTML(roomTokensMinimumBalance)); - - return Rooms.setMinimumTokenBalanceById(rid, minimumTokenBalance); -}; diff --git a/apps/meteor/app/tokenpass/server/functions/updateUserTokenpassBalances.js b/apps/meteor/app/tokenpass/server/functions/updateUserTokenpassBalances.js deleted file mode 100644 index e85992a55dd68..0000000000000 --- a/apps/meteor/app/tokenpass/server/functions/updateUserTokenpassBalances.js +++ /dev/null @@ -1,18 +0,0 @@ -import _ from 'underscore'; - -import { getPublicTokenpassBalances } from './getPublicTokenpassBalances'; -import { getProtectedTokenpassBalances } from './getProtectedTokenpassBalances'; -import { Users } from '../../../models'; - -export const updateUserTokenpassBalances = function (user) { - if (user && user.services && user.services.tokenpass) { - const tcaPublicBalances = getPublicTokenpassBalances(user.services.tokenpass.accessToken); - const tcaProtectedBalances = getProtectedTokenpassBalances(user.services.tokenpass.accessToken); - - const balances = _.uniq(_.union(tcaPublicBalances, tcaProtectedBalances), false, (item) => item.asset); - - Users.setTokenpassTcaBalances(user._id, balances); - - return balances; - } -}; diff --git a/apps/meteor/app/tokenpass/server/index.js b/apps/meteor/app/tokenpass/server/index.js index ba1cf78e09394..086f6cc5d704c 100644 --- a/apps/meteor/app/tokenpass/server/index.js +++ b/apps/meteor/app/tokenpass/server/index.js @@ -1,11 +1,2 @@ import '../lib/common'; import './startup'; -import './functions/getProtectedTokenpassBalances'; -import './functions/getPublicTokenpassBalances'; -import './functions/saveRoomTokensMinimumBalance'; -import './methods/findTokenChannels'; -import './methods/getChannelTokenpass'; -import './cronRemoveUsers'; - -export { updateUserTokenpassBalances } from './functions/updateUserTokenpassBalances'; -export { Tokenpass } from './Tokenpass'; diff --git a/apps/meteor/app/tokenpass/server/methods/findTokenChannels.js b/apps/meteor/app/tokenpass/server/methods/findTokenChannels.js deleted file mode 100644 index c4003aeb12fcf..0000000000000 --- a/apps/meteor/app/tokenpass/server/methods/findTokenChannels.js +++ /dev/null @@ -1,27 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Rooms } from '../../../models'; -import { Tokenpass } from '../Tokenpass'; - -Meteor.methods({ - findTokenChannels() { - if (!Meteor.userId()) { - return []; - } - - const user = Meteor.user(); - - if (user.services && user.services.tokenpass && user.services.tokenpass.tcaBalances) { - const tokens = {}; - user.services.tokenpass.tcaBalances.forEach((token) => { - tokens[token.asset] = 1; - }); - - return Rooms.findByTokenpass(Object.keys(tokens)).filter((room) => - Tokenpass.validateAccess(room.tokenpass, user.services.tokenpass.tcaBalances), - ); - } - - return []; - }, -}); diff --git a/apps/meteor/app/tokenpass/server/methods/getChannelTokenpass.js b/apps/meteor/app/tokenpass/server/methods/getChannelTokenpass.js deleted file mode 100644 index 89a449798781c..0000000000000 --- a/apps/meteor/app/tokenpass/server/methods/getChannelTokenpass.js +++ /dev/null @@ -1,26 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; - -import { Rooms } from '../../../models'; - -Meteor.methods({ - getChannelTokenpass(rid) { - check(rid, String); - - if (!Meteor.userId()) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'getChannelTokenpass', - }); - } - - const room = Rooms.findOneById(rid); - - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { - method: 'getChannelTokenpass', - }); - } - - return room.tokenpass; - }, -}); diff --git a/apps/meteor/app/tokenpass/server/roomAccessValidator.compatibility.js b/apps/meteor/app/tokenpass/server/roomAccessValidator.compatibility.js deleted file mode 100644 index 637a58317532a..0000000000000 --- a/apps/meteor/app/tokenpass/server/roomAccessValidator.compatibility.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Tokenpass } from './Tokenpass'; -import { Users } from '../../models'; - -export function validateTokenAccess(userData, roomData) { - if (!userData || !userData.services || !userData.services.tokenpass || !userData.services.tokenpass.tcaBalances) { - return false; - } - - return Tokenpass.validateAccess(roomData.tokenpass, userData.services.tokenpass.tcaBalances); -} - -export const validators = [ - function (room, user) { - const userData = Users.getTokenBalancesByUserId(user._id); - - return validateTokenAccess(userData, room); - }, -]; diff --git a/apps/meteor/app/tokenpass/server/roomAccessValidator.internalService.ts b/apps/meteor/app/tokenpass/server/roomAccessValidator.internalService.ts deleted file mode 100644 index 4d5bd027ba687..0000000000000 --- a/apps/meteor/app/tokenpass/server/roomAccessValidator.internalService.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { IRoom, IUser } from '@rocket.chat/core-typings'; - -import { ServiceClassInternal } from '../../../server/sdk/types/ServiceClass'; -import { validators } from './roomAccessValidator.compatibility'; -import { IAuthorizationTokenpass } from '../../../server/sdk/types/IAuthorizationTokenpass'; - -export class AuthorizationTokenpass extends ServiceClassInternal implements IAuthorizationTokenpass { - protected name = 'authorization-tokenpass'; - - protected internal = true; - - async canAccessRoom(room: Pick, user: Pick): Promise { - for (const validator of validators) { - if (validator(room, user)) { - return true; - } - } - - return false; - } -} diff --git a/apps/meteor/app/tokenpass/server/startup.js b/apps/meteor/app/tokenpass/server/startup.js index 0d39a0867ec6f..d9f52f6dabd11 100644 --- a/apps/meteor/app/tokenpass/server/startup.js +++ b/apps/meteor/app/tokenpass/server/startup.js @@ -1,11 +1,4 @@ -import { Meteor } from 'meteor/meteor'; -import { Accounts } from 'meteor/accounts-base'; - -import { updateUserTokenpassBalances } from './functions/updateUserTokenpassBalances'; import { settingsRegistry } from '../../settings/server'; -import { callbacks } from '../../../lib/callbacks'; -import { validateTokenAccess } from './roomAccessValidator.compatibility'; -import './roomAccessValidator.internalService'; settingsRegistry.addGroup('OAuth', function () { this.section('Tokenpass', function () { @@ -31,19 +24,3 @@ settingsRegistry.addGroup('OAuth', function () { }); }); }); - -Meteor.startup(function () { - callbacks.add('beforeJoinRoom', function (user, room) { - if (room.tokenpass && !validateTokenAccess(user, room)) { - throw new Meteor.Error('error-not-allowed', 'Token required', { method: 'joinRoom' }); - } - - return user; - }); -}); - -Accounts.onLogin(function ({ user }) { - if (user && user.services && user.services.tokenpass) { - updateUserTokenpassBalances(user); - } -}); diff --git a/apps/meteor/app/ui-sidenav/client/roomList.js b/apps/meteor/app/ui-sidenav/client/roomList.js index 23bb768cf3e82..972ad86ffa07b 100644 --- a/apps/meteor/app/ui-sidenav/client/roomList.js +++ b/apps/meteor/app/ui-sidenav/client/roomList.js @@ -26,7 +26,6 @@ Template.roomList.helpers({ 'settings.preferences.sidebarSortby': 1, 'settings.preferences.sidebarShowFavorites': 1, 'settings.preferences.sidebarShowUnread': 1, - 'services.tokenpass': 1, 'messageViewMode': 1, }, }); @@ -74,12 +73,6 @@ Template.roomList.helpers({ types = ['c', 'p']; } - if (['c', 'p'].includes(this.identifier)) { - query.tokens = { $exists: false }; - } else if (this.identifier === 'tokens' && user && user.services && user.services.tokenpass) { - query.tokens = { $exists: true }; - } - if (getUserPreference(user, 'sidebarShowUnread')) { query.$or = [ { alert: { $ne: true } }, diff --git a/apps/meteor/app/ui/client/views/app/burger.html b/apps/meteor/app/ui/client/views/app/burger.html index 825dcd4f18c25..f854eae0648e1 100644 --- a/apps/meteor/app/ui/client/views/app/burger.html +++ b/apps/meteor/app/ui/client/views/app/burger.html @@ -1,5 +1,5 @@