diff --git a/app/api/server/api.d.ts b/app/api/server/api.d.ts index 9e968448bbc..94b44c774fe 100644 --- a/app/api/server/api.d.ts +++ b/app/api/server/api.d.ts @@ -1,4 +1,118 @@ -import { APIClass } from '.'; +import { Endpoints } from '../../../definition/rest'; +import { Awaited } from '../../../definition/utils'; +import { IUser } from '../../../definition/IUser'; + + +export type ChangeTypeOfKeys< + T extends object, + Keys extends keyof T, + NewType +> = { + [key in keyof T]: key extends Keys ? NewType : T[key] +} + +type This = { + getPaginationItems(): ({ + offset: number; + count: number; + }); + parseJsonQuery(): ({ + sort: Record; + fields: Record; + query: Record; + }); + readonly urlParams: Record; + getUserFromParams(): IUser; +} + +type ThisLoggedIn = { + readonly user: IUser; + readonly userId: string; +} + +type ThisLoggedOut = { + readonly user: null; + readonly userId: null; +} + +type EndpointWithExtraOptions any, A> = WrappedFunction | ({ action: WrappedFunction } & (A extends true ? { twoFactorRequired: boolean } : {})); + +export type Methods = { + [K in keyof T as `${Lowercase}`]: T[K] extends (...args: any) => any ? EndpointWithExtraOptions<(this: This & (A extends true ? ThisLoggedIn : ThisLoggedOut) & Params[0]>) => ReturnType, A> : never; +}; + +type Params = K extends 'GET' ? { readonly queryParams: Partial

} : K extends 'POST' ? { readonly bodyParams: Partial

} : never; + +type SuccessResult = { + statusCode: 200; + success: true; +} & T extends (undefined) ? {} : { body: T } + +type UnauthorizedResult = { + statusCode: 403; + body: { + success: false; + error: string; + }; +} + +type FailureResult = { + statusCode: 400; +} & FailureBody; + +type FailureBody = Exclude extends object ? { body: T & E } : { + body: E & { error: string } & E extends Error ? { details: string } : {} & ST extends undefined ? {} : { stack: string } & ET extends undefined ? {} : { errorType: string }; +} + +type Errors = FailureResult | FailureResult | FailureResult | UnauthorizedResult; + +type WrappedFunction any> = (this: ThisParameterType, ...args: Parameters) => ReturnTypes; + +type ReturnTypes any> = PromisedOrNot> | PromisedOrNot>; + +type PromisedOrNot = Promise | T; + +type Options = { + permissionsRequired?: string[]; + twoFactorOptions?: unknown; + twoFactorRequired?: boolean; + authRequired?: boolean; +} + +export type RestEndpoints

= Methods; + +type ToLowerCaseKeys = { + [K in keyof T as `${Lowercase}`]: T[K]; +}; +type ToResultType = { + [K in keyof T]: T[K] extends (...args: any) => any ? Awaited> : never; +} +export type ResultTypeEndpoints

= ToResultType>; + +declare class APIClass { + addRoute

(route: P, endpoints: RestEndpoints

): void; + + addRoute

(route: P, options: O, endpoints: RestEndpoints): void; + + unauthorized(msg?: string): UnauthorizedResult; + + failure(result: string, errorType?: ET, stack?: ST, error?: E): FailureResult; + + failure(result: object): FailureResult; + + failure(): FailureResult; + + success(): SuccessResult; + + success(result: T): SuccessResult; + + defaultFieldsToExclude: { + joinCode: 0; + members: 0; + importIds: 0; + e2e: 0; + } +} export declare const API: { v1: APIClass; diff --git a/app/api/server/api.js b/app/api/server/api.js index 3b1d6bdfbb9..01a71effe8a 100644 --- a/app/api/server/api.js +++ b/app/api/server/api.js @@ -403,7 +403,7 @@ export class APIClass extends Restivus { api.processTwoFactor({ userId: this.userId, request: this.request, invocation, options: _options.twoFactorOptions, connection }); } - result = DDP._CurrentInvocation.withValue(invocation, () => originalAction.apply(this)) || API.v1.success(); + result = DDP._CurrentInvocation.withValue(invocation, () => Promise.await(originalAction.apply(this))) || API.v1.success(); log.http({ status: result.statusCode, diff --git a/app/api/server/helpers/getPaginationItems.js b/app/api/server/helpers/getPaginationItems.js index 0cff491a976..259f79a1191 100644 --- a/app/api/server/helpers/getPaginationItems.js +++ b/app/api/server/helpers/getPaginationItems.js @@ -10,7 +10,7 @@ API.helperMethods.set('getPaginationItems', function _getPaginationItems() { const offset = this.queryParams.offset ? parseInt(this.queryParams.offset) : 0; let count = defaultCount; - // Ensure count is an appropiate amount + // Ensure count is an appropriate amount if (typeof this.queryParams.count !== 'undefined') { count = parseInt(this.queryParams.count); } else { diff --git a/app/api/server/v1/banners.ts b/app/api/server/v1/banners.ts index 5dd5089814b..678d6e726cf 100644 --- a/app/api/server/v1/banners.ts +++ b/app/api/server/v1/banners.ts @@ -52,7 +52,7 @@ import { BannerPlatform } from '../../../../definition/IBanner'; * $ref: '#/components/schemas/ApiFailureV1' */ API.v1.addRoute('banners.getNew', { authRequired: true }, { // deprecated - get() { + async get() { check(this.queryParams, Match.ObjectIncluding({ platform: String, bid: Match.Maybe(String), @@ -67,7 +67,7 @@ API.v1.addRoute('banners.getNew', { authRequired: true }, { // deprecated throw new Meteor.Error('error-unknown-platform', 'Platform is unknown.'); } - const banners = Promise.await(Banner.getBannersForUser(this.userId, platform, bannerId)); + const banners = await Banner.getBannersForUser(this.userId, platform, bannerId); return API.v1.success({ banners }); }, @@ -119,13 +119,18 @@ API.v1.addRoute('banners.getNew', { authRequired: true }, { // deprecated * schema: * $ref: '#/components/schemas/ApiFailureV1' */ -API.v1.addRoute('banners/:id', { authRequired: true }, { - get() { +API.v1.addRoute('banners/:id', { authRequired: true }, { // TODO: move to users/:id/banners + async get() { check(this.urlParams, Match.ObjectIncluding({ id: String, })); + check(this.queryParams, Match.ObjectIncluding({ + platform: String, + })); + const { platform } = this.queryParams; + if (!platform) { throw new Meteor.Error('error-missing-param', 'The required "platform" param is missing.'); } @@ -135,7 +140,7 @@ API.v1.addRoute('banners/:id', { authRequired: true }, { throw new Meteor.Error('error-missing-param', 'The required "id" param is missing.'); } - const banners = Promise.await(Banner.getBannersForUser(this.userId, platform, id)); + const banners = await Banner.getBannersForUser(this.userId, platform, id); return API.v1.success({ banners }); }, @@ -179,7 +184,7 @@ API.v1.addRoute('banners/:id', { authRequired: true }, { * $ref: '#/components/schemas/ApiFailureV1' */ API.v1.addRoute('banners', { authRequired: true }, { - get() { + async get() { check(this.queryParams, Match.ObjectIncluding({ platform: String, })); @@ -193,7 +198,7 @@ API.v1.addRoute('banners', { authRequired: true }, { throw new Meteor.Error('error-unknown-platform', 'Platform is unknown.'); } - const banners = Promise.await(Banner.getBannersForUser(this.userId, platform)); + const banners = await Banner.getBannersForUser(this.userId, platform); return API.v1.success({ banners }); }, @@ -233,7 +238,7 @@ API.v1.addRoute('banners', { authRequired: true }, { * $ref: '#/components/schemas/ApiFailureV1' */ API.v1.addRoute('banners.dismiss', { authRequired: true }, { - post() { + async post() { check(this.bodyParams, Match.ObjectIncluding({ bannerId: String, })); @@ -244,7 +249,7 @@ API.v1.addRoute('banners.dismiss', { authRequired: true }, { throw new Meteor.Error('error-missing-param', 'The required "bannerId" param is missing.'); } - Promise.await(Banner.dismiss(this.userId, bannerId)); + await Banner.dismiss(this.userId, bannerId); return API.v1.success(); }, }); diff --git a/app/api/server/v1/chat.js b/app/api/server/v1/chat.js index eba0e4e3f66..fa3e917665f 100644 --- a/app/api/server/v1/chat.js +++ b/app/api/server/v1/chat.js @@ -697,7 +697,7 @@ API.v1.addRoute('chat.getSnippetedMessages', { authRequired: true }, { }); API.v1.addRoute('chat.getDiscussions', { authRequired: true }, { - get() { + async get() { const { roomId, text } = this.queryParams; const { sort } = this.parseJsonQuery(); const { offset, count } = this.getPaginationItems(); @@ -705,7 +705,7 @@ API.v1.addRoute('chat.getDiscussions', { authRequired: true }, { if (!roomId) { throw new Meteor.Error('error-invalid-params', 'The required "roomId" query param is missing.'); } - const messages = Promise.await(findDiscussionsFromRoom({ + const messages = await findDiscussionsFromRoom({ uid: this.userId, roomId, text, @@ -714,7 +714,7 @@ API.v1.addRoute('chat.getDiscussions', { authRequired: true }, { count, sort, }, - })); + }); return API.v1.success(messages); }, }); diff --git a/app/api/server/v1/dns.ts b/app/api/server/v1/dns.ts index 902ef90d478..a0b0fa5788e 100644 --- a/app/api/server/v1/dns.ts +++ b/app/api/server/v1/dns.ts @@ -48,7 +48,7 @@ import { resolveSRV, resolveTXT } from '../../../federation/server/functions/res * $ref: '#/components/schemas/ApiFailureV1' */ API.v1.addRoute('dns.resolve.srv', { authRequired: true }, { - get() { + async get() { check(this.queryParams, Match.ObjectIncluding({ url: String, })); @@ -58,7 +58,7 @@ API.v1.addRoute('dns.resolve.srv', { authRequired: true }, { throw new Meteor.Error('error-missing-param', 'The required "url" param is missing.'); } - const resolved = Promise.await(resolveSRV(url)); + const resolved = await resolveSRV(url); return API.v1.success({ resolved }); }, @@ -99,7 +99,7 @@ API.v1.addRoute('dns.resolve.srv', { authRequired: true }, { * $ref: '#/components/schemas/ApiFailureV1' */ API.v1.addRoute('dns.resolve.txt', { authRequired: true }, { - post() { + async post() { check(this.queryParams, Match.ObjectIncluding({ url: String, })); @@ -109,7 +109,7 @@ API.v1.addRoute('dns.resolve.txt', { authRequired: true }, { throw new Meteor.Error('error-missing-param', 'The required "url" param is missing.'); } - const resolved = Promise.await(resolveTXT(url)); + const resolved = await resolveTXT(url); return API.v1.success({ resolved }); }, diff --git a/app/api/server/v1/instances.ts b/app/api/server/v1/instances.ts index d3db3489537..54bd2a563d1 100644 --- a/app/api/server/v1/instances.ts +++ b/app/api/server/v1/instances.ts @@ -5,12 +5,12 @@ import { InstanceStatus } from '../../../models/server/raw'; import { IInstanceStatus } from '../../../../definition/IInstanceStatus'; API.v1.addRoute('instances.get', { authRequired: true }, { - get() { + async get() { if (!hasPermission(this.userId, 'view-statistics')) { return API.v1.unauthorized(); } - const instances = Promise.await(InstanceStatus.find().toArray()); + const instances = await InstanceStatus.find().toArray(); return API.v1.success({ instances: instances.map((instance: IInstanceStatus) => { diff --git a/app/api/server/v1/ldap.ts b/app/api/server/v1/ldap.ts index ee98484d179..c424342d971 100644 --- a/app/api/server/v1/ldap.ts +++ b/app/api/server/v1/ldap.ts @@ -7,7 +7,7 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import { LDAP } from '../../../../server/sdk'; API.v1.addRoute('ldap.testConnection', { authRequired: true }, { - post() { + async post() { if (!this.userId) { throw new Error('error-invalid-user'); } @@ -21,20 +21,20 @@ API.v1.addRoute('ldap.testConnection', { authRequired: true }, { } try { - Promise.await(LDAP.testConnection()); + await LDAP.testConnection(); } catch (error) { SystemLogger.error(error); throw new Error('Connection_failed'); } return API.v1.success({ - message: 'Connection_success', + message: 'Connection_success' as const, }); }, }); API.v1.addRoute('ldap.testSearch', { authRequired: true }, { - post() { + async post() { check(this.bodyParams, Match.ObjectIncluding({ username: String, })); @@ -51,10 +51,10 @@ API.v1.addRoute('ldap.testSearch', { authRequired: true }, { throw new Error('LDAP_disabled'); } - Promise.await(LDAP.testSearch(this.bodyParams.username)); + await LDAP.testSearch(this.bodyParams.username); return API.v1.success({ - message: 'LDAP_User_Found', + message: 'LDAP_User_Found' as const, }); }, }); diff --git a/app/api/server/v1/permissions.ts b/app/api/server/v1/permissions.ts index c2aab9afda5..988f4907e35 100644 --- a/app/api/server/v1/permissions.ts +++ b/app/api/server/v1/permissions.ts @@ -1,12 +1,13 @@ import { Meteor } from 'meteor/meteor'; -import { Match, check } from 'meteor/check'; import { hasPermission } from '../../../authorization/server'; import { API } from '../api'; import { Permissions, Roles } from '../../../models/server/raw'; +import { IPermission } from '../../../../definition/IPermission'; +import { isBodyParamsValidPermissionUpdate } from '../../../../definition/rest/v1/permissions'; API.v1.addRoute('permissions.listAll', { authRequired: true }, { - get() { + async get() { const { updatedSince } = this.queryParams; let updatedSinceDate: Date | undefined; @@ -17,7 +18,10 @@ API.v1.addRoute('permissions.listAll', { authRequired: true }, { updatedSinceDate = new Date(updatedSince); } - const result = Promise.await(Meteor.call('permissions/get', updatedSinceDate)); + const result = await Meteor.call('permissions/get', updatedSinceDate) as { + update: IPermission[]; + remove: IPermission[]; + }; if (Array.isArray(result)) { return API.v1.success({ @@ -31,51 +35,37 @@ API.v1.addRoute('permissions.listAll', { authRequired: true }, { }); API.v1.addRoute('permissions.update', { authRequired: true }, { - post() { + async post() { if (!hasPermission(this.userId, 'access-permissions')) { return API.v1.failure('Editing permissions is not allowed', 'error-edit-permissions-not-allowed'); } - check(this.bodyParams, { - permissions: [ - Match.ObjectIncluding({ - _id: String, - roles: [String], - }), - ], - }); + const { bodyParams } = this; - let permissionNotFound = false; - let roleNotFound = false; - Object.keys(this.bodyParams.permissions).forEach((key) => { - const element = this.bodyParams.permissions[key]; + if (!isBodyParamsValidPermissionUpdate(bodyParams)) { + return API.v1.failure('Invalid body params', 'error-invalid-body-params'); + } - if (!Promise.await(Permissions.findOneById(element._id))) { - permissionNotFound = true; - } + const permissionKeys = bodyParams.permissions.map(({ _id }) => _id); + const permissions = await Permissions.find({ _id: { $in: permissionKeys } }).toArray(); - Object.keys(element.roles).forEach((key) => { - const subElement = element.roles[key]; + if (permissions.length !== bodyParams.permissions.length) { + return API.v1.failure('Invalid permission', 'error-invalid-permission'); + } - if (!Promise.await(Roles.findOneById(subElement))) { - roleNotFound = true; - } - }); - }); + const roleKeys = [...new Set(bodyParams.permissions.flatMap((p) => p.roles))]; - if (permissionNotFound) { - return API.v1.failure('Invalid permission', 'error-invalid-permission'); - } if (roleNotFound) { + const roles = await Roles.find({ _id: { $in: roleKeys } }).toArray(); + + if (roles.length !== roleKeys.length) { return API.v1.failure('Invalid role', 'error-invalid-role'); } - Object.keys(this.bodyParams.permissions).forEach((key) => { - const element = this.bodyParams.permissions[key]; - - Permissions.createOrUpdate(element._id, element.roles); - }); + for await (const permission of bodyParams.permissions) { + await Permissions.setRoles(permission._id, permission.roles); + } - const result = Promise.await(Meteor.call('permissions/get')); + const result = await Meteor.call('permissions/get') as IPermission[]; return API.v1.success({ permissions: result, diff --git a/app/api/server/v1/roles.ts b/app/api/server/v1/roles.ts index 8d87ac7f25c..95c4b34a5d7 100644 --- a/app/api/server/v1/roles.ts +++ b/app/api/server/v1/roles.ts @@ -1,23 +1,25 @@ import { Meteor } from 'meteor/meteor'; -import { Match, check } from 'meteor/check'; import { Users } from '../../../models/server'; import { API } from '../api'; -import { getUsersInRole, hasPermission, hasRole } from '../../../authorization/server'; +import { getUsersInRole, hasRole } from '../../../authorization/server'; import { settings } from '../../../settings/server/index'; import { api } from '../../../../server/sdk/api'; import { Roles } from '../../../models/server/raw'; +import { hasRoleAsync } from '../../../authorization/server/functions/hasRole'; +import { isRoleAddUserToRoleProps, isRoleCreateProps, isRoleDeleteProps, isRoleRemoveUserFromRoleProps, isRoleUpdateProps } from '../../../../definition/rest/v1/roles'; +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; API.v1.addRoute('roles.list', { authRequired: true }, { - get() { - const roles = Promise.await(Roles.find({}, { fields: { _updatedAt: 0 } }).toArray()); + async get() { + const roles = await Roles.find({}, { projection: { _updatedAt: 0 } }).toArray(); return API.v1.success({ roles }); }, }); API.v1.addRoute('roles.sync', { authRequired: true }, { - get() { + async get() { const { updatedSince } = this.queryParams; if (isNaN(Date.parse(updatedSince))) { @@ -26,21 +28,18 @@ API.v1.addRoute('roles.sync', { authRequired: true }, { return API.v1.success({ roles: { - update: Promise.await(Roles.findByUpdatedDate(new Date(updatedSince), { fields: API.v1.defaultFieldsToExclude }).toArray()), - remove: Promise.await(Roles.trashFindDeletedAfter(new Date(updatedSince)).toArray()), + update: await Roles.findByUpdatedDate(new Date(updatedSince)).toArray(), + remove: await Roles.trashFindDeletedAfter(new Date(updatedSince)).toArray(), }, }); }, }); API.v1.addRoute('roles.create', { authRequired: true }, { - post() { - check(this.bodyParams, { - name: String, - scope: Match.Maybe(String), - description: Match.Maybe(String), - mandatory2fa: Match.Maybe(Boolean), - }); + async post() { + if (!isRoleCreateProps(this.bodyParams)) { + throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.'); + } const roleData = { name: this.bodyParams.name, @@ -49,19 +48,18 @@ API.v1.addRoute('roles.create', { authRequired: true }, { mandatory2fa: this.bodyParams.mandatory2fa, }; - if (!hasPermission(Meteor.userId(), 'access-permissions')) { + if (!await hasPermissionAsync(Meteor.userId(), 'access-permissions')) { throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed'); } - if (Promise.await(Roles.findOneByIdOrName(roleData.name))) { + if (await Roles.findOneByIdOrName(roleData.name)) { throw new Meteor.Error('error-duplicate-role-names-not-allowed', 'Role name already exists'); } if (['Users', 'Subscriptions'].includes(roleData.scope) === false) { roleData.scope = 'Users'; } - const a = Roles.createWithRandomId(roleData.name, roleData.scope, roleData.description, false, roleData.mandatory2fa); - const roleId = Promise.await(a).insertedId; + const roleId = (await Roles.createWithRandomId(roleData.name, roleData.scope, roleData.description, false, roleData.mandatory2fa)).insertedId; if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { @@ -70,19 +68,23 @@ API.v1.addRoute('roles.create', { authRequired: true }, { }); } + const role = await Roles.findOneByIdOrName(roleId); + + if (!role) { + return API.v1.failure('error-role-not-found', 'Role not found'); + } + return API.v1.success({ - role: Promise.await(Roles.findOneByIdOrName(roleId, { fields: API.v1.defaultFieldsToExclude })), + role, }); }, }); API.v1.addRoute('roles.addUserToRole', { authRequired: true }, { - post() { - check(this.bodyParams, { - roleName: String, - username: String, - roomId: Match.Maybe(String), - }); + async post() { + if (!isRoleAddUserToRoleProps(this.bodyParams)) { + throw new Meteor.Error('error-invalid-role-properties', isRoleAddUserToRoleProps.errors?.map((error) => error.message).join('\n')); + } const user = this.getUserFromParams(); const { roleName, roomId } = this.bodyParams; @@ -91,22 +93,26 @@ API.v1.addRoute('roles.addUserToRole', { authRequired: true }, { throw new Meteor.Error('error-user-already-in-role', 'User already in role'); } - Meteor.runAsUser(this.userId, () => { - Meteor.call('authorization:addUserToRole', roleName, user.username, roomId); - }); + await Meteor.call('authorization:addUserToRole', roleName, user.username, roomId); + + const role = await Roles.findOneByIdOrName(roleName); + + if (!role) { + return API.v1.failure('error-role-not-found', 'Role not found'); + } return API.v1.success({ - role: Promise.await(Roles.findOneByIdOrName(this.bodyParams.roleName, { fields: API.v1.defaultFieldsToExclude })), + role, }); }, }); API.v1.addRoute('roles.getUsersInRole', { authRequired: true }, { - get() { + async get() { const { roomId, role } = this.queryParams; const { offset, count = 50 } = this.getPaginationItems(); - const fields = { + const projection = { name: 1, username: 1, emails: 1, @@ -116,42 +122,39 @@ API.v1.addRoute('roles.getUsersInRole', { authRequired: true }, { if (!role) { throw new Meteor.Error('error-param-not-provided', 'Query param "role" is required'); } - if (!hasPermission(this.userId, 'access-permissions')) { + if (!await hasPermissionAsync(this.userId, 'access-permissions')) { throw new Meteor.Error('error-not-allowed', 'Not allowed'); } - if (roomId && !hasPermission(this.userId, 'view-other-user-channels')) { + if (roomId && !await hasPermissionAsync(this.userId, 'view-other-user-channels')) { throw new Meteor.Error('error-not-allowed', 'Not allowed'); } - const users = Promise.await(getUsersInRole(role, roomId, { - limit: count, + const users = await getUsersInRole(role, roomId, { + limit: count as number, sort: { username: 1 }, - skip: offset, - fields, - })); + skip: offset as number, + projection, + }); - return API.v1.success({ users: Promise.await(users.toArray()), total: Promise.await(users.count()) }); + return API.v1.success({ users: await users.toArray(), total: await users.count() }); }, }); API.v1.addRoute('roles.update', { authRequired: true }, { - post() { - check(this.bodyParams, { - roleId: String, - name: Match.Maybe(String), - scope: Match.Maybe(String), - description: Match.Maybe(String), - mandatory2fa: Match.Maybe(Boolean), - }); + async post() { + const { bodyParams } = this; + if (!isRoleUpdateProps(bodyParams)) { + throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.'); + } const roleData = { - roleId: this.bodyParams.roleId, - name: this.bodyParams.name, - scope: this.bodyParams.scope, - description: this.bodyParams.description, - mandatory2fa: this.bodyParams.mandatory2fa, + roleId: bodyParams.roleId, + name: bodyParams.name, + scope: bodyParams.scope || 'Users', + description: bodyParams.description, + mandatory2fa: bodyParams.mandatory2fa, }; - const role = Promise.await(Roles.findOneByIdOrName(roleData.roleId)); + const role = await Roles.findOneByIdOrName(roleData.roleId); if (!role) { throw new Meteor.Error('error-invalid-roleId', 'This role does not exist'); @@ -162,19 +165,17 @@ API.v1.addRoute('roles.update', { authRequired: true }, { } if (roleData.name) { - const otherRole = Promise.await(Roles.findOneByIdOrName(roleData.name)); + const otherRole = await Roles.findOneByIdOrName(roleData.name); if (otherRole && otherRole._id !== role._id) { throw new Meteor.Error('error-duplicate-role-names-not-allowed', 'Role name already exists'); } } - if (roleData.scope) { - if (['Users', 'Subscriptions'].includes(roleData.scope) === false) { - roleData.scope = 'Users'; - } + if (['Users', 'Subscriptions'].includes(roleData.scope) === false) { + throw new Meteor.Error('error-invalid-scope', 'Invalid scope'); } - Promise.await(Roles.updateById(roleData.roleId, roleData.name, roleData.scope, roleData.description, roleData.mandatory2fa)); + await Roles.updateById(roleData.roleId, roleData.name, roleData.scope, roleData.description, roleData.mandatory2fa); if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { @@ -183,23 +184,30 @@ API.v1.addRoute('roles.update', { authRequired: true }, { }); } + const updatedRole = await Roles.findOneByIdOrName(roleData.roleId); + + if (!updatedRole) { + return API.v1.failure(); + } + return API.v1.success({ - role: Promise.await(Roles.findOneByIdOrName(roleData.roleId, { fields: API.v1.defaultFieldsToExclude })), + role: updatedRole, }); }, }); API.v1.addRoute('roles.delete', { authRequired: true }, { - post() { - check(this.bodyParams, { - roleId: String, - }); + async post() { + const { bodyParams } = this; + if (!isRoleDeleteProps(bodyParams)) { + throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.'); + } - if (!hasPermission(this.userId, 'access-permissions')) { + if (!await hasPermissionAsync(this.userId, 'access-permissions')) { throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed'); } - const role = Promise.await(Roles.findOneByIdOrName(this.bodyParams.roleId)); + const role = await Roles.findOneByIdOrName(bodyParams.roleId); if (!role) { throw new Meteor.Error('error-invalid-roleId', 'This role does not exist'); @@ -209,33 +217,32 @@ API.v1.addRoute('roles.delete', { authRequired: true }, { throw new Meteor.Error('error-role-protected', 'Cannot delete a protected role'); } - const existingUsers = Promise.await(Roles.findUsersInRole(role.name, role.scope)); + const existingUsers = await Roles.findUsersInRole(role.name, role.scope); - if (existingUsers && Promise.await(existingUsers.count()) > 0) { + if (existingUsers && await existingUsers.count() > 0) { throw new Meteor.Error('error-role-in-use', 'Cannot delete role because it\'s in use'); } - Promise.await(Roles.removeById(role._id)); + await Roles.removeById(role._id); return API.v1.success(); }, }); API.v1.addRoute('roles.removeUserFromRole', { authRequired: true }, { - post() { - check(this.bodyParams, { - roleName: String, - username: String, - scope: Match.Maybe(String), - }); + async post() { + const { bodyParams } = this; + if (!isRoleRemoveUserFromRoleProps(bodyParams)) { + throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.'); + } const data = { - roleName: this.bodyParams.roleName, - username: this.bodyParams.username, + roleName: bodyParams.roleName, + username: bodyParams.username, scope: this.bodyParams.scope, }; - if (!hasPermission(this.userId, 'access-permissions')) { + if (!await hasPermissionAsync(this.userId, 'access-permissions')) { throw new Meteor.Error('error-not-allowed', 'Accessing permissions is not allowed'); } @@ -245,24 +252,24 @@ API.v1.addRoute('roles.removeUserFromRole', { authRequired: true }, { throw new Meteor.Error('error-invalid-user', 'There is no user with this username'); } - const role = Promise.await(Roles.findOneByIdOrName(data.roleName)); + const role = await Roles.findOneByIdOrName(data.roleName); if (!role) { throw new Meteor.Error('error-invalid-roleId', 'This role does not exist'); } - if (!hasRole(user._id, role.name, data.scope)) { + if (!await hasRoleAsync(user._id, role.name, data.scope)) { throw new Meteor.Error('error-user-not-in-role', 'User is not in this role'); } if (role._id === 'admin') { - const adminCount = Promise.await(Promise.await(Roles.findUsersInRole('admin')).count()); + const adminCount = await (await Roles.findUsersInRole('admin')).count(); if (adminCount === 1) { throw new Meteor.Error('error-admin-required', 'You need to have at least one admin'); } } - Promise.await(Roles.removeUserRoles(user._id, [role.name], data.scope)); + await Roles.removeUserRoles(user._id, [role.name], data.scope); if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { diff --git a/app/api/server/v1/settings.ts b/app/api/server/v1/settings.ts index 1f60f2ea2c5..b9233b1e09d 100644 --- a/app/api/server/v1/settings.ts +++ b/app/api/server/v1/settings.ts @@ -1,14 +1,14 @@ import { Meteor } from 'meteor/meteor'; -import { Match, check } from 'meteor/check'; import { ServiceConfiguration } from 'meteor/service-configuration'; import _ from 'underscore'; import { Settings } from '../../../models/server/raw'; import { hasPermission } from '../../../authorization/server'; -import { API } from '../api'; +import { API, ResultTypeEndpoints } from '../api'; import { SettingsEvents, settings } from '../../../settings/server'; import { setValue } from '../../../settings/server/raw'; import { ISetting, ISettingColor, isSettingAction, isSettingColor } from '../../../../definition/ISetting'; +import { isOauthCustomConfiguration, isSettingsUpdatePropDefault, isSettingsUpdatePropsActions, isSettingsUpdatePropsColor } from '../../../../definition/rest/v1/settings'; const fetchSettings = async (query: Parameters[0], sort: Parameters[1]['sort'], offset: Parameters[1]['skip'], count: Parameters[1]['limit'], fields: Parameters[1]['projection']): Promise => { @@ -24,74 +24,35 @@ const fetchSettings = async (query: Parameters[0], sort: P return settings; }; -type OauthCustomConfiguration = { - _id: string; - clientId?: string; - custom: unknown; - service?: string; - serverURL: unknown; - tokenPath: unknown; - identityPath: unknown; - authorizePath: unknown; - scope: unknown; - loginStyle: unknown; - tokenSentVia: unknown; - identityTokenSentVia: unknown; - keyField: unknown; - usernameField: unknown; - emailField: unknown; - nameField: unknown; - avatarField: unknown; - rolesClaim: unknown; - groupsClaim: unknown; - mapChannels: unknown; - channelsMap: unknown; - channelsAdmin: unknown; - mergeUsers: unknown; - mergeRoles: unknown; - accessTokenParam: unknown; - showButton: unknown; - - appId: unknown; - consumerKey: unknown; - - clientConfig: unknown; - buttonLabelText: unknown; - buttonLabelColor: unknown; - buttonColor: unknown; -} -const isOauthCustomConfiguration = (config: any): config is OauthCustomConfiguration => Boolean(config); - // settings endpoints API.v1.addRoute('settings.public', { authRequired: false }, { - get() { + async get() { const { offset, count } = this.getPaginationItems(); const { sort, fields, query } = this.parseJsonQuery(); - let ourQuery = { + const ourQuery = { + ...query, hidden: { $ne: true }, public: true, }; - ourQuery = Object.assign({}, query, ourQuery); - - const settings = Promise.await(fetchSettings(ourQuery, sort, offset, count, fields)); + const settings = await fetchSettings(ourQuery, sort, offset, count, fields); return API.v1.success({ settings, count: settings.length, offset, - total: Settings.find(ourQuery).count(), + total: await Settings.find(ourQuery).count(), }); }, }); API.v1.addRoute('settings.oauth', { authRequired: false }, { get() { - const mountOAuthServices = (): object => { - const oAuthServicesEnabled = ServiceConfiguration.configurations.find({}, { fields: { secret: 0 } }).fetch(); + const oAuthServicesEnabled = ServiceConfiguration.configurations.find({}, { fields: { secret: 0 } }).fetch(); - return oAuthServicesEnabled.map((service) => { + return API.v1.success({ + services: oAuthServicesEnabled.map((service) => { if (!isOauthCustomConfiguration(service)) { return service; } @@ -109,32 +70,25 @@ API.v1.addRoute('settings.oauth', { authRequired: false }, { buttonLabelColor: service.buttonLabelColor || '', custom: false, }; - }); - }; - - return API.v1.success({ - services: mountOAuthServices(), + }), }); }, }); API.v1.addRoute('settings.addCustomOAuth', { authRequired: true, twoFactorRequired: true }, { - post() { - if (!this.requestParams().name || !this.requestParams().name.trim()) { + async post() { + if (!this.bodyParams.name || !this.bodyParams.name.trim()) { throw new Meteor.Error('error-name-param-not-provided', 'The parameter "name" is required'); } - Meteor.runAsUser(this.userId, () => { - Meteor.call('addOAuthService', this.requestParams().name, this.userId); - }); - + await Meteor.call('addOAuthService', this.bodyParams.name, this.userId); return API.v1.success(); }, }); API.v1.addRoute('settings', { authRequired: true }, { - get() { + async get() { const { offset, count } = this.getPaginationItems(); const { sort, fields, query } = this.parseJsonQuery(); @@ -148,7 +102,7 @@ API.v1.addRoute('settings', { authRequired: true }, { ourQuery = Object.assign({}, query, ourQuery); - const settings = Promise.await(fetchSettings(ourQuery, sort, offset, count, fields)); + const settings = await fetchSettings(ourQuery, sort, offset, count, fields); return API.v1.success({ settings, @@ -160,11 +114,11 @@ API.v1.addRoute('settings', { authRequired: true }, { }); API.v1.addRoute('settings/:_id', { authRequired: true }, { - get() { + async get() { if (!hasPermission(this.userId, 'view-privileged-setting')) { return API.v1.unauthorized(); } - const setting = Promise.await(Settings.findOneNotHiddenById(this.urlParams._id)); + const setting = await Settings.findOneNotHiddenById(this.urlParams._id); if (!setting) { return API.v1.failure(); } @@ -172,35 +126,36 @@ API.v1.addRoute('settings/:_id', { authRequired: true }, { }, post: { twoFactorRequired: true, - action(this: any): void { + async action(): Promise['post']> { if (!hasPermission(this.userId, 'edit-privileged-setting')) { return API.v1.unauthorized(); } + if (typeof this.urlParams._id !== 'string') { + throw new Meteor.Error('error-id-param-not-provided', 'The parameter "id" is required'); + } + // allow special handling of particular setting types - const setting = Promise.await(Settings.findOneNotHiddenById(this.urlParams._id)); + const setting = await Settings.findOneNotHiddenById(this.urlParams._id); if (!setting) { return API.v1.failure(); } - if (isSettingAction(setting) && this.bodyParams && this.bodyParams.execute) { + if (isSettingAction(setting) && isSettingsUpdatePropsActions(this.bodyParams) && this.bodyParams.execute) { // execute the configured method Meteor.call(setting.value); return API.v1.success(); } - if (isSettingColor(setting) && this.bodyParams && this.bodyParams.editor && this.bodyParams.value) { + if (isSettingColor(setting) && isSettingsUpdatePropsColor(this.bodyParams)) { Settings.updateOptionsById(this.urlParams._id, { editor: this.bodyParams.editor }); Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value); return API.v1.success(); } - check(this.bodyParams, { - value: Match.Any, - }); - if (Promise.await(Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value))) { - const s = Promise.await(Settings.findOneNotHiddenById(this.urlParams._id)); + if (isSettingsUpdatePropDefault(this.bodyParams) && await Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value)) { + const s = await Settings.findOneNotHiddenById(this.urlParams._id); if (!s) { return API.v1.failure(); } diff --git a/app/api/server/v1/teams.ts b/app/api/server/v1/teams.ts index e2a24d08413..dab3ac1e0a8 100644 --- a/app/api/server/v1/teams.ts +++ b/app/api/server/v1/teams.ts @@ -9,13 +9,22 @@ import { hasAtLeastOnePermission, hasPermission } from '../../../authorization/s import { Users } from '../../../models/server'; import { removeUserFromRoom } from '../../../lib/server/functions/removeUserFromRoom'; import { IUser } from '../../../../definition/IUser'; +import { isTeamPropsWithTeamName, isTeamPropsWithTeamId } from '../../../../definition/rest/v1/teams'; +import { isTeamsConvertToChannelProps } from '../../../../definition/rest/v1/teams/TeamsConvertToChannelProps'; +import { isTeamsRemoveRoomProps } from '../../../../definition/rest/v1/teams/TeamsRemoveRoomProps'; +import { isTeamsUpdateMemberProps } from '../../../../definition/rest/v1/teams/TeamsUpdateMemberProps'; +import { isTeamsRemoveMemberProps } from '../../../../definition/rest/v1/teams/TeamsRemoveMemberProps'; +import { isTeamsAddMembersProps } from '../../../../definition/rest/v1/teams/TeamsAddMembersProps'; +import { isTeamsDeleteProps } from '../../../../definition/rest/v1/teams/TeamsDeleteProps'; +import { isTeamsLeaveProps } from '../../../../definition/rest/v1/teams/TeamsLeaveProps'; +import { isTeamsUpdateProps } from '../../../../definition/rest/v1/teams/TeamsUpdateProps'; API.v1.addRoute('teams.list', { authRequired: true }, { - get() { + async get() { const { offset, count } = this.getPaginationItems(); const { sort, query } = this.parseJsonQuery(); - const { records, total } = Promise.await(Team.list(this.userId, { offset, count }, { sort, query })); + const { records, total } = await Team.list(this.userId, { offset, count }, { sort, query }); return API.v1.success({ teams: records, @@ -27,14 +36,14 @@ API.v1.addRoute('teams.list', { authRequired: true }, { }); API.v1.addRoute('teams.listAll', { authRequired: true }, { - get() { + async get() { if (!hasPermission(this.userId, 'view-all-teams')) { return API.v1.unauthorized(); } - const { offset, count } = this.getPaginationItems(); + const { offset, count } = this.getPaginationItems() as { offset: number; count: number }; - const { records, total } = Promise.await(Team.listAll({ offset, count })); + const { records, total } = await Team.listAll({ offset, count }); return API.v1.success({ teams: records, @@ -46,7 +55,7 @@ API.v1.addRoute('teams.listAll', { authRequired: true }, { }); API.v1.addRoute('teams.create', { authRequired: true }, { - post() { + async post() { if (!hasPermission(this.userId, 'create-team')) { return API.v1.unauthorized(); } @@ -56,7 +65,7 @@ API.v1.addRoute('teams.create', { authRequired: true }, { return API.v1.failure('Body param "name" is required'); } - const team = Promise.await(Team.create(this.userId, { + const team = await Team.create(this.userId, { team: { name, type, @@ -64,26 +73,27 @@ API.v1.addRoute('teams.create', { authRequired: true }, { room, members, owner, - })); + }); return API.v1.success({ team }); }, }); API.v1.addRoute('teams.convertToChannel', { authRequired: true }, { - post() { - check(this.bodyParams, Match.ObjectIncluding({ - teamId: Match.Maybe(String), - teamName: Match.Maybe(String), - roomsToRemove: Match.Maybe([String]), - })); - const { roomsToRemove, teamId, teamName } = this.bodyParams; - - if (!teamId && !teamName) { - return API.v1.failure('missing-teamId-or-teamName'); + async post() { + if (!isTeamsConvertToChannelProps(this.bodyParams)) { + return API.v1.failure('invalid-body-params', isTeamsConvertToChannelProps.errors?.map((e) => e.message).join('\n ')); } - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); + const { bodyParams } = this; + + const { roomsToRemove = [] } = bodyParams; + + const team = await ( + (isTeamPropsWithTeamId(bodyParams) && Team.getOneById(bodyParams.teamId)) + || (isTeamPropsWithTeamName(bodyParams) && Team.getOneByName(bodyParams.teamName)) + ); + if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -92,7 +102,7 @@ API.v1.addRoute('teams.convertToChannel', { authRequired: true }, { return API.v1.unauthorized(); } - const rooms: string[] = Promise.await(Team.getMatchingTeamRooms(team._id, roomsToRemove)); + const rooms = await Team.getMatchingTeamRooms(team._id, roomsToRemove); if (rooms.length) { rooms.forEach((room) => { @@ -100,7 +110,7 @@ API.v1.addRoute('teams.convertToChannel', { authRequired: true }, { }); } - Promise.all([ + await Promise.all([ Team.unsetTeamIdOfRooms(team._id), Team.removeAllMembersFromTeam(team._id), Team.deleteById(team._id), @@ -111,14 +121,14 @@ API.v1.addRoute('teams.convertToChannel', { authRequired: true }, { }); API.v1.addRoute('teams.addRooms', { authRequired: true }, { - post() { + async post() { const { rooms, teamId, teamName } = this.bodyParams; if (!teamId && !teamName) { return API.v1.failure('missing-teamId-or-teamName'); } - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); + const team = await (teamId ? Team.getOneById(teamId) : Team.getOneByName(teamName)); if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -127,17 +137,20 @@ API.v1.addRoute('teams.addRooms', { authRequired: true }, { return API.v1.unauthorized('error-no-permission-team-channel'); } - const validRooms = Promise.await(Team.addRooms(this.userId, rooms, team._id)); + const validRooms = await Team.addRooms(this.userId, rooms, team._id); return API.v1.success({ rooms: validRooms }); }, }); API.v1.addRoute('teams.removeRoom', { authRequired: true }, { - post() { + async post() { + if (!isTeamsRemoveRoomProps(this.bodyParams)) { + return API.v1.failure('body-params-invalid', isTeamsRemoveRoomProps.errors?.map((error) => error.message).join('\n ')); + } const { roomId, teamId, teamName } = this.bodyParams; - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); + const team = await (teamId ? Team.getOneById(teamId) : Team.getOneByName(teamName)); if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -148,17 +161,17 @@ API.v1.addRoute('teams.removeRoom', { authRequired: true }, { const canRemoveAny = !!hasPermission(this.userId, 'view-all-team-channels', team.roomId); - const room = Promise.await(Team.removeRoom(this.userId, roomId, team._id, canRemoveAny)); + const room = await Team.removeRoom(this.userId, roomId, team._id, canRemoveAny); return API.v1.success({ room }); }, }); API.v1.addRoute('teams.updateRoom', { authRequired: true }, { - post() { + async post() { const { roomId, isDefault } = this.bodyParams; - const team = Promise.await(Team.getOneByRoomId(roomId)); + const team = await Team.getOneByRoomId(roomId); if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -168,18 +181,18 @@ API.v1.addRoute('teams.updateRoom', { authRequired: true }, { } const canUpdateAny = !!hasPermission(this.userId, 'view-all-team-channels', team.roomId); - const room = Promise.await(Team.updateRoom(this.userId, roomId, isDefault, canUpdateAny)); + const room = await Team.updateRoom(this.userId, roomId, isDefault, canUpdateAny); return API.v1.success({ room }); }, }); API.v1.addRoute('teams.listRooms', { authRequired: true }, { - get() { + async get() { const { teamId, teamName, filter, type } = this.queryParams; const { offset, count } = this.getPaginationItems(); - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); + const team = await (teamId ? Team.getOneById(teamId) : Team.getOneByName(teamName)); if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -198,7 +211,7 @@ API.v1.addRoute('teams.listRooms', { authRequired: true }, { allowPrivateTeam, }; - const { records, total } = Promise.await(Team.listRooms(this.userId, team._id, listFilter, { offset, count })); + const { records, total } = await Team.listRooms(this.userId, team._id, listFilter, { offset, count }); return API.v1.success({ rooms: records, @@ -210,11 +223,17 @@ API.v1.addRoute('teams.listRooms', { authRequired: true }, { }); API.v1.addRoute('teams.listRoomsOfUser', { authRequired: true }, { - get() { + async get() { const { offset, count } = this.getPaginationItems(); const { teamId, teamName, userId, canUserDelete = false } = this.queryParams; - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); + + if (!teamId && !teamName) { + return API.v1.failure('missing-teamId-or-teamName'); + } + + const team = await (teamId ? Team.getOneById(teamId) : Team.getOneByName(teamName!)); + if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -225,7 +244,7 @@ API.v1.addRoute('teams.listRoomsOfUser', { authRequired: true }, { return API.v1.unauthorized(); } - const { records, total } = Promise.await(Team.listRoomsOfUser(this.userId, team._id, userId, allowPrivateTeam, canUserDelete, { offset, count })); + const { records, total } = await Team.listRoomsOfUser(this.userId, team._id, userId, allowPrivateTeam, canUserDelete, { offset, count }); return API.v1.success({ rooms: records, @@ -237,7 +256,7 @@ API.v1.addRoute('teams.listRoomsOfUser', { authRequired: true }, { }); API.v1.addRoute('teams.members', { authRequired: true }, { - get() { + async get() { const { offset, count } = this.getPaginationItems(); check(this.queryParams, Match.ObjectIncluding({ @@ -253,7 +272,7 @@ API.v1.addRoute('teams.members', { authRequired: true }, { return API.v1.failure('missing-teamId-or-teamName'); } - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); + const team = await (teamId ? Team.getOneById(teamId) : Team.getOneByName(teamName)); if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -265,7 +284,7 @@ API.v1.addRoute('teams.members', { authRequired: true }, { status: status ? { $in: status } : undefined, } as FilterQuery; - const { records, total } = Promise.await(Team.members(this.userId, team._id, canSeeAllMembers, { offset, count }, query)); + const { records, total } = await Team.members(this.userId, team._id, canSeeAllMembers, { offset, count }, query); return API.v1.success({ members: records, @@ -277,10 +296,19 @@ API.v1.addRoute('teams.members', { authRequired: true }, { }); API.v1.addRoute('teams.addMembers', { authRequired: true }, { - post() { - const { teamId, teamName, members } = this.bodyParams; + async post() { + if (!isTeamsAddMembersProps(this.bodyParams)) { + return API.v1.failure('invalid-params'); + } + + const { bodyParams } = this; + const { members } = bodyParams; + + const team = await ( + (isTeamPropsWithTeamId(bodyParams) && Team.getOneById(bodyParams.teamId)) + || (isTeamPropsWithTeamName(bodyParams) && Team.getOneByName(bodyParams.teamName)) + ); - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -289,17 +317,25 @@ API.v1.addRoute('teams.addMembers', { authRequired: true }, { return API.v1.unauthorized(); } - Promise.await(Team.addMembers(this.userId, team._id, members)); + await Team.addMembers(this.userId, team._id, members); return API.v1.success(); }, }); API.v1.addRoute('teams.updateMember', { authRequired: true }, { - post() { - const { teamId, teamName, member } = this.bodyParams; + async post() { + if (!isTeamsUpdateMemberProps(this.bodyParams)) { + return API.v1.failure('invalid-params', isTeamsUpdateMemberProps.errors?.map((e) => e.message).join('\n ')); + } - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); + const { bodyParams } = this; + const { member } = bodyParams; + + const team = await ( + (isTeamPropsWithTeamId(bodyParams) && Team.getOneById(bodyParams.teamId)) + || (isTeamPropsWithTeamName(bodyParams) && Team.getOneByName(bodyParams.teamName)) + ); if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -308,17 +344,26 @@ API.v1.addRoute('teams.updateMember', { authRequired: true }, { return API.v1.unauthorized(); } - Promise.await(Team.updateMember(team._id, member)); + await Team.updateMember(team._id, member); return API.v1.success(); }, }); API.v1.addRoute('teams.removeMember', { authRequired: true }, { - post() { - const { teamId, teamName, userId, rooms } = this.bodyParams; + async post() { + if (!isTeamsRemoveMemberProps(this.bodyParams)) { + return API.v1.failure('invalid-params', isTeamsRemoveMemberProps.errors?.map((e) => e.message).join('\n ')); + } + + const { bodyParams } = this; + const { userId, rooms } = bodyParams; + + const team = await ( + (isTeamPropsWithTeamId(bodyParams) && Team.getOneById(bodyParams.teamId)) + || (isTeamPropsWithTeamName(bodyParams) && Team.getOneByName(bodyParams.teamName)) + ); - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -332,12 +377,12 @@ API.v1.addRoute('teams.removeMember', { authRequired: true }, { return API.v1.failure('invalid-user'); } - if (!Promise.await(Team.removeMembers(this.userId, team._id, [{ userId }]))) { + if (!await Team.removeMembers(this.userId, team._id, [{ userId }])) { return API.v1.failure(); } if (rooms?.length) { - const roomsFromTeam: string[] = Promise.await(Team.getMatchingTeamRooms(team._id, rooms)); + const roomsFromTeam: string[] = await Team.getMatchingTeamRooms(team._id, rooms); roomsFromTeam.forEach((rid) => { removeUserFromRoom(rid, user, { @@ -350,20 +395,30 @@ API.v1.addRoute('teams.removeMember', { authRequired: true }, { }); API.v1.addRoute('teams.leave', { authRequired: true }, { - post() { - const { teamId, teamName, rooms } = this.bodyParams; + async post() { + if (!isTeamsLeaveProps(this.bodyParams)) { + return API.v1.failure('invalid-params', isTeamsLeaveProps.errors?.map((e) => e.message).join('\n ')); + } + + const { bodyParams } = this; + + const { rooms = [] } = bodyParams; + + const team = await ( + (isTeamPropsWithTeamId(bodyParams) && Team.getOneById(bodyParams.teamId)) + || (isTeamPropsWithTeamName(bodyParams) && Team.getOneByName(bodyParams.teamName)) + ); - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); if (!team) { return API.v1.failure('team-does-not-exist'); } - Promise.await(Team.removeMembers(this.userId, team._id, [{ + await Team.removeMembers(this.userId, team._id, [{ userId: this.userId, - }])); + }]); - if (rooms?.length) { - const roomsFromTeam: string[] = Promise.await(Team.getMatchingTeamRooms(team._id, rooms)); + if (rooms.length) { + const roomsFromTeam: string[] = await Team.getMatchingTeamRooms(team._id, rooms); roomsFromTeam.forEach((rid) => { removeUserFromRoom(rid, this.user); @@ -375,16 +430,16 @@ API.v1.addRoute('teams.leave', { authRequired: true }, { }); API.v1.addRoute('teams.info', { authRequired: true }, { - get() { + async get() { const { teamId, teamName } = this.queryParams; if (!teamId && !teamName) { return API.v1.failure('Provide either the "teamId" or "teamName"'); } - const teamInfo = teamId - ? Promise.await(Team.getInfoById(teamId)) - : Promise.await(Team.getInfoByName(teamName)); + const teamInfo = await (teamId + ? Team.getInfoById(teamId) + : Team.getInfoByName(teamName)); if (!teamInfo) { return API.v1.failure('Team not found'); @@ -395,18 +450,19 @@ API.v1.addRoute('teams.info', { authRequired: true }, { }); API.v1.addRoute('teams.delete', { authRequired: true }, { - post() { - const { teamId, teamName, roomsToRemove } = this.bodyParams; + async post() { + const { bodyParams } = this; + const { roomsToRemove = [] } = this.bodyParams; - if (!teamId && !teamName) { - return API.v1.failure('Provide either the "teamId" or "teamName"'); + if (!isTeamsDeleteProps(bodyParams)) { + return API.v1.failure('invalid-params', isTeamsDeleteProps.errors?.map((e) => e.message).join('\n ')); } - if (roomsToRemove && !Array.isArray(roomsToRemove)) { - return API.v1.failure('The list of rooms to remove is invalid.'); - } + const team = await ( + (isTeamPropsWithTeamId(bodyParams) && Team.getOneById(bodyParams.teamId)) + || (isTeamPropsWithTeamName(bodyParams) && Team.getOneByName(bodyParams.teamName)) + ); - const team = teamId ? Promise.await(Team.getOneById(teamId)) : Promise.await(Team.getOneByName(teamName)); if (!team) { return API.v1.failure('Team not found.'); } @@ -415,7 +471,7 @@ API.v1.addRoute('teams.delete', { authRequired: true }, { return API.v1.unauthorized(); } - const rooms: string[] = Promise.await(Team.getMatchingTeamRooms(team._id, roomsToRemove)); + const rooms: string[] = await Team.getMatchingTeamRooms(team._id, roomsToRemove); // Remove the team's main room Meteor.call('eraseRoom', team.roomId); @@ -428,41 +484,42 @@ API.v1.addRoute('teams.delete', { authRequired: true }, { } // Move every other room back to the workspace - Promise.await(Team.unsetTeamIdOfRooms(team._id)); + await Team.unsetTeamIdOfRooms(team._id); // Delete all team memberships - Team.removeAllMembersFromTeam(teamId); + Team.removeAllMembersFromTeam(team._id); // And finally delete the team itself - Promise.await(Team.deleteById(team._id)); + await Team.deleteById(team._id); return API.v1.success(); }, }); API.v1.addRoute('teams.autocomplete', { authRequired: true }, { - get() { + async get() { const { name } = this.queryParams; - const teams = Promise.await(Team.autocomplete(this.userId, name)); + const teams = await Team.autocomplete(this.userId, name); return API.v1.success({ teams }); }, }); API.v1.addRoute('teams.update', { authRequired: true }, { - post() { - check(this.bodyParams, { - teamId: String, - data: { - name: Match.Maybe(String), - type: Match.Maybe(Number), - }, - }); + async post() { + const { bodyParams } = this; + if (!isTeamsUpdateProps(bodyParams)) { + return API.v1.failure('invalid-params', isTeamsUpdateProps.errors?.map((e) => e.message).join('\n ')); + } + + const { data } = bodyParams; - const { teamId, data } = this.bodyParams; + const team = await ( + (isTeamPropsWithTeamId(bodyParams) && Team.getOneById(bodyParams.teamId)) + || (isTeamPropsWithTeamName(bodyParams) && Team.getOneByName(bodyParams.teamName)) + ); - const team = teamId && Promise.await(Team.getOneById(teamId)); if (!team) { return API.v1.failure('team-does-not-exist'); } @@ -471,7 +528,7 @@ API.v1.addRoute('teams.update', { authRequired: true }, { return API.v1.unauthorized(); } - Promise.await(Team.update(this.userId, teamId, { name: data.name, type: data.type })); + await Team.update(this.userId, team._id, data); return API.v1.success(); }, diff --git a/app/authorization/server/functions/getUsersInRole.ts b/app/authorization/server/functions/getUsersInRole.ts index 8a8bcadf3db..740431af3f0 100644 --- a/app/authorization/server/functions/getUsersInRole.ts +++ b/app/authorization/server/functions/getUsersInRole.ts @@ -9,6 +9,6 @@ export function getUsersInRole(name: IRole['name'], scope?: string): Promise>): Promise>; -export function getUsersInRole

(name: IRole['name'], scope: string | undefined, options: FindOneOptions

): Promise>; +export function getUsersInRole

(name: IRole['name'], scope: string | undefined, options: FindOneOptions

): Promise>; -export function getUsersInRole

(name: IRole['name'], scope: string | undefined, options?: any | undefined): Promise> { return Roles.findUsersInRole(name, scope, options); } +export function getUsersInRole

(name: IRole['name'], scope: string | undefined, options?: any | undefined): Promise> { return Roles.findUsersInRole(name, scope, options); } diff --git a/app/models/server/raw/BaseRaw.ts b/app/models/server/raw/BaseRaw.ts index 500fe55d6e1..1385bf890c6 100644 --- a/app/models/server/raw/BaseRaw.ts +++ b/app/models/server/raw/BaseRaw.ts @@ -166,7 +166,7 @@ export class BaseRaw = undefined> implements IBase find(query: FilterQuery, options: WithoutProjection>): Cursor>; - find

(query: FilterQuery, options: FindOneOptions

): Cursor

; + find

(query: FilterQuery, options: FindOneOptions

): Cursor

; find

(query: FilterQuery | undefined = {}, options?: any): Cursor

| Cursor { const optionsDef = this.doNotMixInclusionAndExclusionFields(options); diff --git a/app/models/server/raw/Permissions.ts b/app/models/server/raw/Permissions.ts index 0c5ae1f8533..1b0bacacc53 100644 --- a/app/models/server/raw/Permissions.ts +++ b/app/models/server/raw/Permissions.ts @@ -30,6 +30,10 @@ export class PermissionsRaw extends BaseRaw { await this.update({ _id: permission, roles: { $ne: role } }, { $addToSet: { roles: role } }); } + async setRoles(permission: string, roles: string[]): Promise { + await this.update({ _id: permission }, { $set: { roles } }); + } + async removeRole(permission: string, role: string): Promise { await this.update({ _id: permission, roles: role }, { $pull: { roles: role } }); } diff --git a/app/models/server/raw/Roles.ts b/app/models/server/raw/Roles.ts index db7913eb7e2..c858d2445ce 100644 --- a/app/models/server/raw/Roles.ts +++ b/app/models/server/raw/Roles.ts @@ -17,12 +17,12 @@ export class RolesRaw extends BaseRaw { } - findByUpdatedDate

(updatedAfterDate: Date, options: FindOneOptions

): Cursor

| Cursor { + findByUpdatedDate(updatedAfterDate: Date, options?: FindOneOptions): Cursor { const query = { _updatedAt: { $gte: new Date(updatedAfterDate) }, }; - return this.find(query, options); + return options ? this.find(query, options) : this.find(query); } @@ -135,7 +135,7 @@ export class RolesRaw extends BaseRaw { return this.findOne(query, options); } - updateById(_id: IRole['_id'], name: IRole['name'], scope: IRole['scope'], description: IRole['description'], mandatory2fa: IRole['mandatory2fa']): Promise { + updateById(_id: IRole['_id'], name: IRole['name'], scope: IRole['scope'], description: IRole['description'] = '', mandatory2fa: IRole['mandatory2fa'] = false): Promise { const queryData = { name, scope, @@ -151,7 +151,7 @@ export class RolesRaw extends BaseRaw { findUsersInRole(name: IRole['name'], scope: string | undefined, options: WithoutProjection>): Promise>; - findUsersInRole

(name: IRole['name'], scope: string | undefined, options: FindOneOptions

): Promise>; + findUsersInRole

(name: IRole['name'], scope: string | undefined, options: FindOneOptions

): Promise>; async findUsersInRole

(name: IRole['name'], scope: string | undefined, options?: any | undefined): Promise | Cursor

> { const role = await this.findOne({ name }, { scope: 1 } as FindOneOptions); diff --git a/app/models/server/raw/Settings.ts b/app/models/server/raw/Settings.ts index 2d3a11b2cce..7e84d539b05 100644 --- a/app/models/server/raw/Settings.ts +++ b/app/models/server/raw/Settings.ts @@ -116,7 +116,7 @@ export class SettingsRaw extends BaseRaw { filter._id = { $in: ids }; } - return this.find(filter, { fields: { _id: 1, value: 1, editor: 1, enterprise: 1, invalidValue: 1, modules: 1, requiredOnWizard: 1 } }) as any; + return this.find(filter, { projection: { _id: 1, value: 1, editor: 1, enterprise: 1, invalidValue: 1, modules: 1, requiredOnWizard: 1 } }); } findSetupWizardSettings(): Cursor { diff --git a/client/contexts/ServerContext/ServerContext.ts b/client/contexts/ServerContext/ServerContext.ts index d791fea0d2d..15b8c50f97f 100644 --- a/client/contexts/ServerContext/ServerContext.ts +++ b/client/contexts/ServerContext/ServerContext.ts @@ -2,7 +2,7 @@ import { createContext, useCallback, useContext, useMemo } from 'react'; import { IServerInfo } from '../../../definition/IServerInfo'; import type { Serialized } from '../../../definition/Serialized'; -import type { PathFor, Params, Return, Method } from './endpoints'; +import type { PathFor, Params, Return, Method } from '../../../definition/rest'; import { ServerMethodFunction, ServerMethodName, diff --git a/client/contexts/ServerContext/endpoints.ts b/client/contexts/ServerContext/endpoints.ts deleted file mode 100644 index 0a57ef4479d..00000000000 --- a/client/contexts/ServerContext/endpoints.ts +++ /dev/null @@ -1,87 +0,0 @@ -import type { ExtractKeys, ValueOf } from '../../../definition/utils'; -import type { EngagementDashboardEndpoints } from '../../../ee/client/contexts/ServerContext/endpoints/v1/engagementDashboard'; -import type { AppsEndpoints } from './endpoints/apps'; -import type { ChannelsEndpoints } from './endpoints/v1/channels'; -import type { ChatEndpoints } from './endpoints/v1/chat'; -import type { CloudEndpoints } from './endpoints/v1/cloud'; -import type { CustomUserStatusEndpoints } from './endpoints/v1/customUserStatus'; -import type { DmEndpoints } from './endpoints/v1/dm'; -import type { DnsEndpoints } from './endpoints/v1/dns'; -import type { EmojiCustomEndpoints } from './endpoints/v1/emojiCustom'; -import type { GroupsEndpoints } from './endpoints/v1/groups'; -import type { ImEndpoints } from './endpoints/v1/im'; -import type { LDAPEndpoints } from './endpoints/v1/ldap'; -import type { LicensesEndpoints } from './endpoints/v1/licenses'; -import type { MiscEndpoints } from './endpoints/v1/misc'; -import type { OmnichannelEndpoints } from './endpoints/v1/omnichannel'; -import type { RoomsEndpoints } from './endpoints/v1/rooms'; -import type { StatisticsEndpoints } from './endpoints/v1/statistics'; -import type { TeamsEndpoints } from './endpoints/v1/teams'; -import type { UsersEndpoints } from './endpoints/v1/users'; - -type Endpoints = ChatEndpoints & - ChannelsEndpoints & - CloudEndpoints & - CustomUserStatusEndpoints & - DmEndpoints & - DnsEndpoints & - EmojiCustomEndpoints & - GroupsEndpoints & - ImEndpoints & - LDAPEndpoints & - RoomsEndpoints & - TeamsEndpoints & - UsersEndpoints & - EngagementDashboardEndpoints & - AppsEndpoints & - OmnichannelEndpoints & - StatisticsEndpoints & - LicensesEndpoints & - MiscEndpoints; - -type Endpoint = UnionizeEndpoints; - -type UnionizeEndpoints = ValueOf< - { - [P in keyof EE]: UnionizeMethods; - } ->; - -type ExtractOperations = ExtractKeys any>; - -type UnionizeMethods = ValueOf< - { - [M in keyof OO as ExtractOperations]: ( - method: M, - path: OO extends { path: string } ? OO['path'] : P, - ...params: Parameters any>> - ) => ReturnType any>>; - } ->; - -export type Method = Parameters[0]; -export type Path = Parameters[1]; - -export type MethodFor

= P extends any - ? Parameters any>>[0] - : never; -export type PathFor = M extends any - ? Parameters any>>[1] - : never; - -type Operation> = M extends any - ? P extends any - ? Extract any> - : never - : never; - -type ExtractParams = Q extends [any, any] - ? [undefined?] - : Q extends [any, any, any, ...any[]] - ? [Q[2]] - : never; - -export type Params> = ExtractParams< - Parameters> ->; -export type Return> = ReturnType>; diff --git a/client/contexts/ServerContext/endpoints/v1/dm.ts b/client/contexts/ServerContext/endpoints/v1/dm.ts deleted file mode 100644 index 0b20aad1819..00000000000 --- a/client/contexts/ServerContext/endpoints/v1/dm.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { IRoom } from '../../../../../definition/IRoom'; -import type { IUser } from '../../../../../definition/IUser'; - -export type DmEndpoints = { - 'dm.create': { - POST: ( - params: ( - | { - username: Exclude; - } - | { - usernames: string; - } - ) & { - excludeSelf?: boolean; - }, - ) => { - room: IRoom & { rid: IRoom['_id'] }; - }; - }; -}; diff --git a/client/contexts/ServerContext/endpoints/v1/teams.ts b/client/contexts/ServerContext/endpoints/v1/teams.ts deleted file mode 100644 index 70f8a7c10b1..00000000000 --- a/client/contexts/ServerContext/endpoints/v1/teams.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { IRoom } from '../../../../../definition/IRoom'; -import type { IRecordsWithTotal, ITeam } from '../../../../../definition/ITeam'; -import type { IUser } from '../../../../../definition/IUser'; - -export type TeamsEndpoints = { - 'teams.addRooms': { - POST: (params: { rooms: IRoom['_id'][]; teamId: string }) => void; - }; - 'teams.info': { - GET: (params: { teamId: IRoom['teamId'] }) => { teamInfo: ITeam }; - }; - 'teams.listRooms': { - GET: (params: { - teamId: ITeam['_id']; - offset?: number; - count?: number; - filter: string; - type: string; - }) => Omit, 'records'> & { - count: number; - offset: number; - rooms: IRecordsWithTotal['records']; - }; - }; - 'teams.listRoomsOfUser': { - GET: (params: { - teamId: ITeam['_id']; - teamName?: string; - userId?: string; - canUserDelete?: boolean; - offset?: number; - count?: number; - }) => Omit, 'records'> & { - count: number; - offset: number; - rooms: IRecordsWithTotal['records']; - }; - }; - 'teams.create': { - POST: (params: { - name: ITeam['name']; - type?: ITeam['type']; - members?: IUser['_id'][]; - room: { - id?: string; - name?: IRoom['name']; - members?: IUser['_id'][]; - readOnly?: boolean; - extraData?: { - teamId?: string; - teamMain?: boolean; - } & { [key: string]: string | boolean }; - options?: { - nameValidationRegex?: string; - creator: string; - subscriptionExtra?: { - open: boolean; - ls: Date; - prid: IRoom['_id']; - }; - } & { - [key: string]: - | string - | { - open: boolean; - ls: Date; - prid: IRoom['_id']; - }; - }; - }; - owner?: IUser['_id']; - }) => { - team: ITeam; - }; - }; -}; diff --git a/client/contexts/ServerContext/index.ts b/client/contexts/ServerContext/index.ts index a2807467fdd..cd41c0b16c1 100644 --- a/client/contexts/ServerContext/index.ts +++ b/client/contexts/ServerContext/index.ts @@ -1,3 +1,2 @@ export * from './ServerContext'; -export * from './endpoints'; export * from './methods'; diff --git a/client/hooks/useEndpointAction.ts b/client/hooks/useEndpointAction.ts index 65a7f2ccaed..0deb6cdd7d7 100644 --- a/client/hooks/useEndpointAction.ts +++ b/client/hooks/useEndpointAction.ts @@ -1,8 +1,8 @@ import { useCallback } from 'react'; import { Serialized } from '../../definition/Serialized'; +import { Method, Params, PathFor, Return } from '../../definition/rest'; import { useEndpoint } from '../contexts/ServerContext'; -import { Method, Params, PathFor, Return } from '../contexts/ServerContext/endpoints'; import { useToastMessageDispatch } from '../contexts/ToastMessagesContext'; export const useEndpointAction = >( diff --git a/client/hooks/useEndpointActionExperimental.ts b/client/hooks/useEndpointActionExperimental.ts index b6cb286f46a..0e82f6d4283 100644 --- a/client/hooks/useEndpointActionExperimental.ts +++ b/client/hooks/useEndpointActionExperimental.ts @@ -1,8 +1,8 @@ import { useCallback } from 'react'; import { Serialized } from '../../definition/Serialized'; +import { Method, Params, PathFor, Return } from '../../definition/rest'; import { useEndpoint } from '../contexts/ServerContext'; -import { Method, Params, PathFor, Return } from '../contexts/ServerContext/endpoints'; import { useToastMessageDispatch } from '../contexts/ToastMessagesContext'; export const useEndpointActionExperimental = >( diff --git a/client/hooks/useEndpointData.ts b/client/hooks/useEndpointData.ts index 6412cd9d7f3..1e8f5ebc880 100644 --- a/client/hooks/useEndpointData.ts +++ b/client/hooks/useEndpointData.ts @@ -1,8 +1,8 @@ import { useCallback, useEffect } from 'react'; import { Serialized } from '../../definition/Serialized'; +import { Params, PathFor, Return } from '../../definition/rest'; import { useEndpoint } from '../contexts/ServerContext'; -import { Params, PathFor, Return } from '../contexts/ServerContext/endpoints'; import { useToastMessageDispatch } from '../contexts/ToastMessagesContext'; import { AsyncState, useAsyncState } from './useAsyncState'; diff --git a/client/providers/ServerProvider.tsx b/client/providers/ServerProvider.tsx index 89b1c28b959..43f600d2262 100644 --- a/client/providers/ServerProvider.tsx +++ b/client/providers/ServerProvider.tsx @@ -3,11 +3,8 @@ import React, { FC } from 'react'; import { Info as info, APIClient } from '../../app/utils/client'; import { Serialized } from '../../definition/Serialized'; +import { Method, Params, Return, PathFor } from '../../definition/rest'; import { - Method, - Params, - PathFor, - Return, ServerContext, ServerMethodName, ServerMethodParameters, diff --git a/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts b/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts index 6f144e0400a..c310c75b73a 100644 --- a/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts +++ b/client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts @@ -41,9 +41,10 @@ export const useTeamsChannelList = ( }); return { - items: rooms.map(({ _updatedAt, lastMessage, lm, jitsiTimeout, ...room }) => ({ + items: rooms.map(({ _updatedAt, lastMessage, lm, ts, jitsiTimeout, ...room }) => ({ jitsiTimeout: new Date(jitsiTimeout), ...(lm && { lm: new Date(lm) }), + ...(ts && { ts: new Date(ts) }), _updatedAt: new Date(_updatedAt), ...(lastMessage && { lastMessage: mapMessageFromApi(lastMessage) }), ...room, diff --git a/definition/IMessage/IMessage.ts b/definition/IMessage/IMessage.ts index 55f46c28386..8fa8b66c348 100644 --- a/definition/IMessage/IMessage.ts +++ b/definition/IMessage/IMessage.ts @@ -1,11 +1,11 @@ import { MessageSurfaceLayout } from '@rocket.chat/ui-kit'; import { parser } from '@rocket.chat/message-parser'; -import { IRocketChatRecord } from '../IRocketChatRecord'; -import { IUser } from '../IUser'; -import { ChannelName, RoomID } from '../IRoom'; -import { MessageAttachment } from './MessageAttachment/MessageAttachment'; -import { FileProp } from './MessageAttachment/Files/FileProp'; +import type { IRocketChatRecord } from '../IRocketChatRecord'; +import type { IUser } from '../IUser'; +import type { ChannelName, RoomID } from '../IRoom'; +import type { MessageAttachment } from './MessageAttachment/MessageAttachment'; +import type { FileProp } from './MessageAttachment/Files/FileProp'; type MentionType = 'user' | 'team'; diff --git a/definition/IRoom.ts b/definition/IRoom.ts index 8597a9035e8..04e94c1cb40 100644 --- a/definition/IRoom.ts +++ b/definition/IRoom.ts @@ -63,6 +63,7 @@ export interface IRoom extends IRocketChatRecord { muted?: string[]; usernames?: string[]; + ts?: Date; } export interface ICreatedRoom extends IRoom { diff --git a/definition/IUser.ts b/definition/IUser.ts index e40908cec83..55d1b62b29b 100644 --- a/definition/IUser.ts +++ b/definition/IUser.ts @@ -95,7 +95,7 @@ export interface IRole { name: string; protected: boolean; // scope?: string; - scope?: 'Users' | 'Subscriptions'; + scope: 'Users' | 'Subscriptions'; _id: string; } diff --git a/client/contexts/ServerContext/endpoints/apps.ts b/definition/rest/apps/index.ts similarity index 100% rename from client/contexts/ServerContext/endpoints/apps.ts rename to definition/rest/apps/index.ts diff --git a/definition/rest/helpers/PaginatedRequest.ts b/definition/rest/helpers/PaginatedRequest.ts new file mode 100644 index 00000000000..44962dea206 --- /dev/null +++ b/definition/rest/helpers/PaginatedRequest.ts @@ -0,0 +1,4 @@ +export type PaginatedRequest = { + count: number; + offset: number; +}; diff --git a/definition/rest/helpers/PaginatedResult.ts b/definition/rest/helpers/PaginatedResult.ts new file mode 100644 index 00000000000..e78980c0d1e --- /dev/null +++ b/definition/rest/helpers/PaginatedResult.ts @@ -0,0 +1,5 @@ +export type PaginatedResult = { + count: number; + offset: number; + total: number; +}; diff --git a/definition/rest/index.ts b/definition/rest/index.ts new file mode 100644 index 00000000000..b7bad4b5ffd --- /dev/null +++ b/definition/rest/index.ts @@ -0,0 +1,97 @@ +import type { EnterpriseEndpoints } from '../../ee/definition/rest'; +import type { ExtractKeys, ValueOf } from '../utils'; +import type { AppsEndpoints } from './apps'; +import { BannersEndpoints } from './v1/banners'; +import type { ChannelsEndpoints } from './v1/channels'; +import type { ChatEndpoints } from './v1/chat'; +import type { CloudEndpoints } from './v1/cloud'; +import type { CustomUserStatusEndpoints } from './v1/customUserStatus'; +import type { DmEndpoints } from './v1/dm'; +import type { DnsEndpoints } from './v1/dns'; +import type { EmojiCustomEndpoints } from './v1/emojiCustom'; +import type { GroupsEndpoints } from './v1/groups'; +import type { ImEndpoints } from './v1/im'; +import { InstancesEndpoints } from './v1/instances'; +import type { LDAPEndpoints } from './v1/ldap'; +import type { LicensesEndpoints } from './v1/licenses'; +import type { MiscEndpoints } from './v1/misc'; +import type { OmnichannelEndpoints } from './v1/omnichannel'; +import { PermissionsEndpoints } from './v1/permissions'; +import { RolesEndpoints } from './v1/roles'; +import type { RoomsEndpoints } from './v1/rooms'; +import { SettingsEndpoints } from './v1/settings'; +import type { StatisticsEndpoints } from './v1/statistics'; +import type { TeamsEndpoints } from './v1/teams'; +import type { UsersEndpoints } from './v1/users'; + +type CommunityEndpoints = BannersEndpoints & ChatEndpoints & +ChannelsEndpoints & +CloudEndpoints & +CustomUserStatusEndpoints & +DmEndpoints & +DnsEndpoints & +EmojiCustomEndpoints & +GroupsEndpoints & +ImEndpoints & +LDAPEndpoints & +RoomsEndpoints & +RolesEndpoints & +TeamsEndpoints & +SettingsEndpoints & +UsersEndpoints & +AppsEndpoints & +OmnichannelEndpoints & +StatisticsEndpoints & +LicensesEndpoints & +MiscEndpoints & +PermissionsEndpoints & +InstancesEndpoints; + +export type Endpoints = CommunityEndpoints & EnterpriseEndpoints; + +type Endpoint = UnionizeEndpoints; + +type UnionizeEndpoints = ValueOf< +{ + [P in keyof EE]: UnionizeMethods; +} +>; + +type ExtractOperations = ExtractKeys any>; + +type UnionizeMethods = ValueOf< +{ + [M in keyof OO as ExtractOperations]: ( + method: M, + path: OO extends { path: string } ? OO['path'] : P, + ...params: Parameters any>> + ) => ReturnType any>>; +} +>; + +export type Method = Parameters[0]; +export type Path = Parameters[1]; + +export type MethodFor

= P extends any + ? Parameters any>>[0] + : never; +export type PathFor = M extends any + ? Parameters any>>[1] + : never; + +type Operation> = M extends any + ? P extends any + ? Extract any> + : never + : never; + +type ExtractParams = Q extends [any, any] + ? [undefined?] + : Q extends [any, any, any, ...any[]] + ? [Q[2]] + : never; + +export type Params> = ExtractParams< +Parameters> +>; +export type Return> = ReturnType>; diff --git a/definition/rest/v1/banners.ts b/definition/rest/v1/banners.ts new file mode 100644 index 00000000000..e448abb7b41 --- /dev/null +++ b/definition/rest/v1/banners.ts @@ -0,0 +1,26 @@ +import { IBanner } from '../../IBanner'; + +export type BannersEndpoints = { + /* @deprecated */ + 'banners.getNew': { + GET: () => ({ + banners: IBanner[]; + }); + }; + + 'banners/:id': { + GET: (params: { platform: string }) => ({ + banners: IBanner[]; + }); + }; + + 'banners': { + GET: () => ({ + banners: IBanner[]; + }); + }; + + 'banners.dismiss': { + POST: (params: { bannerId: string }) => void; + }; +}; diff --git a/client/contexts/ServerContext/endpoints/v1/channels.ts b/definition/rest/v1/channels.ts similarity index 70% rename from client/contexts/ServerContext/endpoints/v1/channels.ts rename to definition/rest/v1/channels.ts index 0e2abbf7751..1a5bbca27ba 100644 --- a/client/contexts/ServerContext/endpoints/v1/channels.ts +++ b/definition/rest/v1/channels.ts @@ -1,6 +1,6 @@ -import type { IMessage } from '../../../../../definition/IMessage/IMessage'; -import type { IRoom } from '../../../../../definition/IRoom'; -import type { IUser } from '../../../../../definition/IUser'; +import type { IMessage } from '../../IMessage/IMessage'; +import type { IRoom } from '../../IRoom'; +import type { IUser } from '../../IUser'; export type ChannelsEndpoints = { 'channels.files': { diff --git a/client/contexts/ServerContext/endpoints/v1/chat.ts b/definition/rest/v1/chat.ts similarity index 84% rename from client/contexts/ServerContext/endpoints/v1/chat.ts rename to definition/rest/v1/chat.ts index 0ed5c1e0298..134f832b64e 100644 --- a/client/contexts/ServerContext/endpoints/v1/chat.ts +++ b/definition/rest/v1/chat.ts @@ -1,5 +1,5 @@ -import type { IMessage } from '../../../../../definition/IMessage'; -import type { IRoom } from '../../../../../definition/IRoom'; +import type { IMessage } from '../../IMessage'; +import type { IRoom } from '../../IRoom'; export type ChatEndpoints = { 'chat.getMessage': { diff --git a/client/contexts/ServerContext/endpoints/v1/cloud.ts b/definition/rest/v1/cloud.ts similarity index 100% rename from client/contexts/ServerContext/endpoints/v1/cloud.ts rename to definition/rest/v1/cloud.ts diff --git a/client/contexts/ServerContext/endpoints/v1/customUserStatus.ts b/definition/rest/v1/customUserStatus.ts similarity index 100% rename from client/contexts/ServerContext/endpoints/v1/customUserStatus.ts rename to definition/rest/v1/customUserStatus.ts diff --git a/definition/rest/v1/dm.ts b/definition/rest/v1/dm.ts new file mode 100644 index 00000000000..4ce71b05469 --- /dev/null +++ b/definition/rest/v1/dm.ts @@ -0,0 +1,21 @@ +import type { IRoom } from '../../IRoom'; +import type { IUser } from '../../IUser'; + +export type DmEndpoints = { + 'dm.create': { + POST: ( + params: ( + | { + username: Exclude; + } + | { + usernames: string; + } + ) & { + excludeSelf?: boolean; + }, + ) => { + room: IRoom & { rid: IRoom['_id'] }; + }; + }; +}; diff --git a/client/contexts/ServerContext/endpoints/v1/dns.ts b/definition/rest/v1/dns.ts similarity index 62% rename from client/contexts/ServerContext/endpoints/v1/dns.ts rename to definition/rest/v1/dns.ts index 136e8d698a9..b2d553e036f 100644 --- a/client/contexts/ServerContext/endpoints/v1/dns.ts +++ b/definition/rest/v1/dns.ts @@ -5,8 +5,9 @@ export type DnsEndpoints = { }; }; 'dns.resolve.txt': { - GET: (params: { url: string }) => { - resolved: Record; + POST: (params: { url: string }) => { + resolved: string; + // resolved: Record; }; }; }; diff --git a/client/contexts/ServerContext/endpoints/v1/emojiCustom.ts b/definition/rest/v1/emojiCustom.ts similarity index 73% rename from client/contexts/ServerContext/endpoints/v1/emojiCustom.ts rename to definition/rest/v1/emojiCustom.ts index 63648e6f3e2..0a49286ecfd 100644 --- a/client/contexts/ServerContext/endpoints/v1/emojiCustom.ts +++ b/definition/rest/v1/emojiCustom.ts @@ -1,4 +1,4 @@ -import type { ICustomEmojiDescriptor } from '../../../../../definition/ICustomEmojiDescriptor'; +import type { ICustomEmojiDescriptor } from '../../ICustomEmojiDescriptor'; export type EmojiCustomEndpoints = { 'emoji-custom.list': { diff --git a/client/contexts/ServerContext/endpoints/v1/groups.ts b/definition/rest/v1/groups.ts similarity index 69% rename from client/contexts/ServerContext/endpoints/v1/groups.ts rename to definition/rest/v1/groups.ts index 5b01d443d45..3b5a584795a 100644 --- a/client/contexts/ServerContext/endpoints/v1/groups.ts +++ b/definition/rest/v1/groups.ts @@ -1,6 +1,6 @@ -import type { IMessage } from '../../../../../definition/IMessage'; -import type { IRoom } from '../../../../../definition/IRoom'; -import type { IUser } from '../../../../../definition/IUser'; +import type { IMessage } from '../../IMessage'; +import type { IRoom } from '../../IRoom'; +import type { IUser } from '../../IUser'; export type GroupsEndpoints = { 'groups.files': { diff --git a/client/contexts/ServerContext/endpoints/v1/im.ts b/definition/rest/v1/im.ts similarity index 65% rename from client/contexts/ServerContext/endpoints/v1/im.ts rename to definition/rest/v1/im.ts index 700cdecfda4..b88a5b2be0c 100644 --- a/client/contexts/ServerContext/endpoints/v1/im.ts +++ b/definition/rest/v1/im.ts @@ -1,17 +1,17 @@ -import type { IMessage } from '../../../../../definition/IMessage'; -import type { IRoom } from '../../../../../definition/IRoom'; -import type { IUser } from '../../../../../definition/IUser'; +import type { IMessage } from '../../IMessage'; +import type { IRoom } from '../../IRoom'; +import type { IUser } from '../../IUser'; export type ImEndpoints = { 'im.create': { POST: ( params: ( | { - username: Exclude; - } + username: Exclude; + } | { - usernames: string; - } + usernames: string; + } ) & { excludeSelf?: boolean; }, diff --git a/definition/rest/v1/instances.ts b/definition/rest/v1/instances.ts new file mode 100644 index 00000000000..c792fc9b5fd --- /dev/null +++ b/definition/rest/v1/instances.ts @@ -0,0 +1,16 @@ +import { IInstanceStatus } from '../../IInstanceStatus'; + +export type InstancesEndpoints = { + 'instances.get': { + GET: () => ({ + instances: (IInstanceStatus | { + connection: { + address: unknown; + currentStatus: unknown; + instanceRecord: unknown; + broadcastAuth: unknown; + }; + })[]; + }); + }; +}; diff --git a/client/contexts/ServerContext/endpoints/v1/ldap.ts b/definition/rest/v1/ldap.ts similarity index 64% rename from client/contexts/ServerContext/endpoints/v1/ldap.ts rename to definition/rest/v1/ldap.ts index 09b19d5637a..7553d749eaf 100644 --- a/client/contexts/ServerContext/endpoints/v1/ldap.ts +++ b/definition/rest/v1/ldap.ts @@ -1,4 +1,4 @@ -import type { TranslationKey } from '../../../TranslationContext'; +import type { TranslationKey } from '../../../client/contexts/TranslationContext'; export type LDAPEndpoints = { 'ldap.testConnection': { @@ -7,9 +7,9 @@ export type LDAPEndpoints = { }; }; 'ldap.testSearch': { - POST: (params: { username: string }) => { + POST: (params: { username: string }) => ({ message: TranslationKey; - }; + }); }; 'ldap.syncNow': { POST: () => { diff --git a/client/contexts/ServerContext/endpoints/v1/licenses.ts b/definition/rest/v1/licenses.ts similarity index 64% rename from client/contexts/ServerContext/endpoints/v1/licenses.ts rename to definition/rest/v1/licenses.ts index 5d78d69dd5e..cc4b0dba981 100644 --- a/client/contexts/ServerContext/endpoints/v1/licenses.ts +++ b/definition/rest/v1/licenses.ts @@ -1,9 +1,12 @@ -import type { ILicense } from '../../../../../ee/app/license/server/license'; +import type { ILicense } from '../../../ee/app/license/definitions/ILicense'; export type LicensesEndpoints = { 'licenses.get': { GET: () => { licenses: Array }; }; + 'licenses.add': { + POST: (params: { license: string }) => void; + }; 'licenses.maxActiveUsers': { GET: () => { maxActiveUsers: number | null; activeUsers: number }; }; diff --git a/client/contexts/ServerContext/endpoints/v1/misc.ts b/definition/rest/v1/misc.ts similarity index 100% rename from client/contexts/ServerContext/endpoints/v1/misc.ts rename to definition/rest/v1/misc.ts diff --git a/client/contexts/ServerContext/endpoints/v1/omnichannel.ts b/definition/rest/v1/omnichannel.ts similarity index 82% rename from client/contexts/ServerContext/endpoints/v1/omnichannel.ts rename to definition/rest/v1/omnichannel.ts index 4254db71253..1598355cb5b 100644 --- a/client/contexts/ServerContext/endpoints/v1/omnichannel.ts +++ b/definition/rest/v1/omnichannel.ts @@ -1,10 +1,10 @@ -import { ILivechatDepartment } from '../../../../../definition/ILivechatDepartment'; -import { ILivechatMonitor } from '../../../../../definition/ILivechatMonitor'; -import { ILivechatTag } from '../../../../../definition/ILivechatTag'; -import { IOmnichannelCannedResponse } from '../../../../../definition/IOmnichannelCannedResponse'; -import { IOmnichannelRoom, IRoom } from '../../../../../definition/IRoom'; -import { ISetting } from '../../../../../definition/ISetting'; -import { IUser } from '../../../../../definition/IUser'; +import { ILivechatDepartment } from '../../ILivechatDepartment'; +import { ILivechatMonitor } from '../../ILivechatMonitor'; +import { ILivechatTag } from '../../ILivechatTag'; +import { IOmnichannelCannedResponse } from '../../IOmnichannelCannedResponse'; +import { IOmnichannelRoom, IRoom } from '../../IRoom'; +import { ISetting } from '../../ISetting'; +import { IUser } from '../../IUser'; export type OmnichannelEndpoints = { 'livechat/appearance': { @@ -49,7 +49,7 @@ export type OmnichannelEndpoints = { }; }; 'livechat/department/:_id': { - path: `livechat/department/${string}`; + path: `livechat/department/${ string }`; GET: () => { department: ILivechatDepartment; }; @@ -138,7 +138,7 @@ export type OmnichannelEndpoints = { DELETE: (params: { _id: IOmnichannelCannedResponse['_id'] }) => void; }; 'canned-responses/:_id': { - path: `canned-responses/${string}`; + path: `canned-responses/${ string }`; GET: () => { cannedResponse: IOmnichannelCannedResponse; }; diff --git a/definition/rest/v1/permissions.ts b/definition/rest/v1/permissions.ts new file mode 100644 index 00000000000..b9ec5ca9586 --- /dev/null +++ b/definition/rest/v1/permissions.ts @@ -0,0 +1,43 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +import { IPermission } from '../../IPermission'; + +const ajv = new Ajv(); + +type PermissionsUpdateProps = { permissions: { _id: string; roles: string[] }[] }; + +const permissionUpdatePropsSchema: JSONSchemaType = { + type: 'object', + properties: { + permissions: { + type: 'array', + items: { + type: 'object', + properties: { + _id: { type: 'string' }, + roles: { type: 'array', items: { type: 'string' }, uniqueItems: true }, + }, + additionalProperties: false, + required: ['_id', 'roles'], + }, + }, + }, + required: ['permissions'], + additionalProperties: false, +}; + +export const isBodyParamsValidPermissionUpdate = ajv.compile(permissionUpdatePropsSchema); + +export type PermissionsEndpoints = { + 'permissions.listAll': { + GET: (params: { updatedSince?: string }) => ({ + update: IPermission[]; + remove: IPermission[]; + }); + }; + 'permissions.update': { + POST: (params: PermissionsUpdateProps) => ({ + permissions: IPermission[]; + }); + }; +}; diff --git a/definition/rest/v1/roles.ts b/definition/rest/v1/roles.ts new file mode 100644 index 00000000000..844d125083b --- /dev/null +++ b/definition/rest/v1/roles.ts @@ -0,0 +1,186 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +import { IRole, IUser } from '../../IUser'; + +const ajv = new Ajv(); + +type RoleCreateProps = Pick & Partial>; + +const roleCreatePropsSchema: JSONSchemaType = { + type: 'object', + properties: { + name: { + type: 'string', + }, + description: { + type: 'string', + nullable: true, + }, + scope: { + type: 'string', + enum: ['Users', 'Subscriptions'], + nullable: true, + }, + mandatory2fa: { + type: 'boolean', + nullable: true, + }, + }, + required: ['name'], + additionalProperties: false, +}; + +export const isRoleCreateProps = ajv.compile(roleCreatePropsSchema); + +type RoleUpdateProps = { roleId: IRole['_id']; name: IRole['name'] } & Partial; + +const roleUpdatePropsSchema: JSONSchemaType = { + type: 'object', + properties: { + roleId: { + type: 'string', + }, + name: { + type: 'string', + }, + description: { + type: 'string', + nullable: true, + }, + scope: { + type: 'string', + enum: ['Users', 'Subscriptions'], + nullable: true, + }, + mandatory2fa: { + type: 'boolean', + nullable: true, + }, + }, + required: ['roleId', 'name'], + additionalProperties: false, +}; + +export const isRoleUpdateProps = ajv.compile(roleUpdatePropsSchema); + +type RoleDeleteProps = { roleId: IRole['_id'] }; + +const roleDeletePropsSchema: JSONSchemaType = { + type: 'object', + properties: { + roleId: { + type: 'string', + }, + }, + required: ['roleId'], + additionalProperties: false, +}; + +export const isRoleDeleteProps = ajv.compile(roleDeletePropsSchema); + +type RoleAddUserToRoleProps = { + username: string; + roleName: string; + roomId?: string; +} + +const roleAddUserToRolePropsSchema: JSONSchemaType = { + type: 'object', + properties: { + username: { + type: 'string', + }, + roleName: { + type: 'string', + }, + roomId: { + type: 'string', + nullable: true, + }, + }, + required: ['username', 'roleName'], + additionalProperties: false, +}; + + +export const isRoleAddUserToRoleProps = ajv.compile(roleAddUserToRolePropsSchema); + +type RoleRemoveUserFromRoleProps = { + username: string; + roleName: string; + roomId?: string; +} + +const roleRemoveUserFromRolePropsSchema: JSONSchemaType = { + type: 'object', + properties: { + username: { + type: 'string', + }, + roleName: { + type: 'string', + }, + roomId: { + type: 'string', + nullable: true, + }, + }, + required: ['username', 'roleName'], + additionalProperties: false, +}; + +export const isRoleRemoveUserFromRoleProps = ajv.compile(roleRemoveUserFromRolePropsSchema); + +type RoleSyncProps = { + updatedSince?: string; +} + +export type RolesEndpoints = { + 'roles.list': { + GET: () => ({ + roles: IRole[]; + }); + }; + 'roles.sync': { + GET: (params: RoleSyncProps) => ({ + roles: { + update: IRole[]; + remove: IRole[]; + }; + }); + }; + 'roles.create': { + POST: (params: RoleCreateProps) => ({ + role: IRole; + }); + }; + + 'roles.addUserToRole': { + POST: (params: RoleAddUserToRoleProps) => ({ + role: IRole; + }); + }; + + 'roles.getUsersInRole': { + GET: (params: { roomId: string; role: string; offset: number; count: number }) => ({ + users: IUser[]; + total: number; + }); + }; + + 'roles.update': { + POST: (role: RoleUpdateProps) => ({ + role: IRole; + }); + }; + + 'roles.delete': { + POST: (prop: RoleDeleteProps) => void; + }; + + 'roles.removeUserFromRole': { + POST: (props: RoleRemoveUserFromRoleProps) => ({ + role: IRole; + }); + }; +}; diff --git a/client/contexts/ServerContext/endpoints/v1/rooms.ts b/definition/rest/v1/rooms.ts similarity index 81% rename from client/contexts/ServerContext/endpoints/v1/rooms.ts rename to definition/rest/v1/rooms.ts index 960610ec558..92f0bc5895c 100644 --- a/client/contexts/ServerContext/endpoints/v1/rooms.ts +++ b/definition/rest/v1/rooms.ts @@ -1,6 +1,6 @@ -import type { IMessage } from '../../../../../definition/IMessage'; -import type { IRoom } from '../../../../../definition/IRoom'; -import type { IUser } from '../../../../../definition/IUser'; +import type { IMessage } from '../../IMessage'; +import type { IRoom } from '../../IRoom'; +import type { IUser } from '../../IUser'; export type RoomsEndpoints = { 'rooms.autocomplete.channelAndPrivate': { diff --git a/definition/rest/v1/settings.ts b/definition/rest/v1/settings.ts new file mode 100644 index 00000000000..71ffab95bbf --- /dev/null +++ b/definition/rest/v1/settings.ts @@ -0,0 +1,100 @@ +import { ISetting, ISettingColor } from '../../ISetting'; +import { PaginatedResult } from '../helpers/PaginatedResult'; + +type SettingsUpdateProps = SettingsUpdatePropDefault | SettingsUpdatePropsActions | SettingsUpdatePropsColor; + +type SettingsUpdatePropsActions = { + execute: boolean; +} + +export type OauthCustomConfiguration = { + _id: string; + clientId?: string; + custom: unknown; + service?: string; + serverURL: unknown; + tokenPath: unknown; + identityPath: unknown; + authorizePath: unknown; + scope: unknown; + loginStyle: unknown; + tokenSentVia: unknown; + identityTokenSentVia: unknown; + keyField: unknown; + usernameField: unknown; + emailField: unknown; + nameField: unknown; + avatarField: unknown; + rolesClaim: unknown; + groupsClaim: unknown; + mapChannels: unknown; + channelsMap: unknown; + channelsAdmin: unknown; + mergeUsers: unknown; + mergeRoles: unknown; + accessTokenParam: unknown; + showButton: unknown; + + appId: unknown; + consumerKey?: string; + + clientConfig: unknown; + buttonLabelText: unknown; + buttonLabelColor: unknown; + buttonColor: unknown; +} + +export const isOauthCustomConfiguration = (config: any): config is OauthCustomConfiguration => Boolean(config); + +export const isSettingsUpdatePropsActions = (props: Partial): props is SettingsUpdatePropsActions => 'execute' in props; + +type SettingsUpdatePropsColor = { + editor: ISettingColor['editor']; + value: ISetting['value']; +} + +export const isSettingsUpdatePropsColor = (props: Partial): props is SettingsUpdatePropsColor => 'editor' in props && 'value' in props; + +type SettingsUpdatePropDefault = { + value: ISetting['value']; +} + +export const isSettingsUpdatePropDefault = (props: Partial): props is SettingsUpdatePropDefault => 'value' in props; + +export type SettingsEndpoints = { + 'settings.public': { + GET: () => PaginatedResult & { + settings: Array; + }; + }; + + 'settings.oauth': { + GET: () => ({ + services: Partial[]; + }); + }; + + 'settings.addCustomOAuth': { + POST: (params: { name: string }) => void; + }; + + 'settings': { + GET: () => ({ + settings: ISetting[]; + }); + }; + + 'settings/:_id': { + GET: () => Pick; + POST: (params: SettingsUpdateProps) => void; + }; + + 'service.configurations': { + GET: () => { + configurations: Array<{ + appId: string; + secret: string; + }>; + }; + }; +}; diff --git a/client/contexts/ServerContext/endpoints/v1/statistics.ts b/definition/rest/v1/statistics.ts similarity index 63% rename from client/contexts/ServerContext/endpoints/v1/statistics.ts rename to definition/rest/v1/statistics.ts index 178d8f5d66b..5820d2be290 100644 --- a/client/contexts/ServerContext/endpoints/v1/statistics.ts +++ b/definition/rest/v1/statistics.ts @@ -1,4 +1,4 @@ -import type { IStats } from '../../../../../definition/IStats'; +import type { IStats } from '../../IStats'; export type StatisticsEndpoints = { statistics: { diff --git a/definition/rest/v1/teams/TeamsAddMembersProps.test.ts b/definition/rest/v1/teams/TeamsAddMembersProps.test.ts new file mode 100644 index 00000000000..f2d7ec71bc2 --- /dev/null +++ b/definition/rest/v1/teams/TeamsAddMembersProps.test.ts @@ -0,0 +1,71 @@ +/* eslint-env mocha */ +import chai from 'chai'; + +import { isTeamsAddMembersProps } from './TeamsAddMembersProps'; + +describe('TeamsAddMemberProps (definition/rest/v1)', () => { + describe('isTeamsAddMembersProps', () => { + it('should be a function', () => { + chai.assert.isFunction(isTeamsAddMembersProps); + }); + it('should return false if the parameter is empty', () => { + chai.assert.isFalse(isTeamsAddMembersProps({})); + }); + + it('should return false if teamId is provided but no member was provided', () => { + chai.assert.isFalse(isTeamsAddMembersProps({ teamId: '123' })); + }); + + it('should return false if teamName is provided but no member was provided', () => { + chai.assert.isFalse(isTeamsAddMembersProps({ teamName: '123' })); + }); + + it('should return false if members is provided but no teamId or teamName were provided', () => { + chai.assert.isFalse(isTeamsAddMembersProps({ members: [{ userId: '123' }] })); + }); + + it('should return false if teamName was provided but members are empty', () => { + chai.assert.isFalse(isTeamsAddMembersProps({ teamName: '123', members: [] })); + }); + + it('should return false if teamId was provided but members are empty', () => { + chai.assert.isFalse(isTeamsAddMembersProps({ teamId: '123', members: [] })); + }); + + it('should return false if members with role is provided but no teamId or teamName were provided', () => { + chai.assert.isFalse(isTeamsAddMembersProps({ members: [{ userId: '123', roles: ['123'] }] })); + }); + + it('should return true if members is provided and teamId is provided', () => { + chai.assert.isTrue(isTeamsAddMembersProps({ members: [{ userId: '123' }], teamId: '123' })); + }); + + it('should return true if members is provided and teamName is provided', () => { + chai.assert.isTrue(isTeamsAddMembersProps({ members: [{ userId: '123' }], teamName: '123' })); + }); + + it('should return true if members with role is provided and teamId is provided', () => { + chai.assert.isTrue(isTeamsAddMembersProps({ members: [{ userId: '123', roles: ['123'] }], teamId: '123' })); + }); + + it('should return true if members with role is provided and teamName is provided', () => { + chai.assert.isTrue(isTeamsAddMembersProps({ members: [{ userId: '123', roles: ['123'] }], teamName: '123' })); + }); + + it('should return false if teamName was provided and members contains an invalid property', () => { + chai.assert.isFalse(isTeamsAddMembersProps({ teamName: '123', members: [{ userId: '123', roles: ['123'], invalid: true }] })); + }); + + it('should return false if teamId was provided and members contains an invalid property', () => { + chai.assert.isFalse(isTeamsAddMembersProps({ teamId: '123', members: [{ userId: '123', roles: ['123'], invalid: true }] })); + }); + + it('should return false if teamName informed but contains an invalid property', () => { + chai.assert.isFalse(isTeamsAddMembersProps({ member: [{ userId: '123', roles: ['123'] }], teamName: '123', invalid: true })); + }); + + it('should return false if teamId informed but contains an invalid property', () => { + chai.assert.isFalse(isTeamsAddMembersProps({ member: [{ userId: '123', roles: ['123'] }], teamId: '123', invalid: true })); + }); + }); +}); diff --git a/definition/rest/v1/teams/TeamsAddMembersProps.ts b/definition/rest/v1/teams/TeamsAddMembersProps.ts new file mode 100644 index 00000000000..599989fbf7a --- /dev/null +++ b/definition/rest/v1/teams/TeamsAddMembersProps.ts @@ -0,0 +1,80 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +import { ITeamMemberParams } from '../../../../server/sdk/types/ITeamService'; + +const ajv = new Ajv(); + +export type TeamsAddMembersProps = ({ teamId: string } | { teamName: string }) & { members: ITeamMemberParams[] }; + +const teamsAddMembersPropsSchema: JSONSchemaType = { + oneOf: [ + { + type: 'object', + properties: { + teamId: { + type: 'string', + }, + members: { + type: 'array', + items: { + + type: 'object', + properties: { + userId: { + type: 'string', + }, + roles: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + }, + required: ['userId'], + additionalProperties: false, + }, + minItems: 1, + uniqueItems: true, + }, + }, + required: ['teamId', 'members'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + teamName: { + type: 'string', + }, + members: { + type: 'array', + items: { + + type: 'object', + properties: { + userId: { + type: 'string', + }, + roles: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + }, + required: ['userId'], + additionalProperties: false, + }, + minItems: 1, + uniqueItems: true, + }, + }, + required: ['teamName', 'members'], + additionalProperties: false, + }, + ], +}; + +export const isTeamsAddMembersProps = ajv.compile(teamsAddMembersPropsSchema); diff --git a/definition/rest/v1/teams/TeamsConvertToChannelProps.test.ts b/definition/rest/v1/teams/TeamsConvertToChannelProps.test.ts new file mode 100644 index 00000000000..5c292c6f1b3 --- /dev/null +++ b/definition/rest/v1/teams/TeamsConvertToChannelProps.test.ts @@ -0,0 +1,39 @@ +/* eslint-env mocha */ +import chai from 'chai'; + +import { isTeamsConvertToChannelProps } from './TeamsConvertToChannelProps'; + +describe('TeamsConvertToChannelProps (definition/rest/v1)', () => { + describe('isTeamsConvertToChannelProps', () => { + it('should be a function', () => { + chai.assert.isFunction(isTeamsConvertToChannelProps); + }); + it('should return false if neither teamName or teamId is provided', () => { + chai.assert.isFalse(isTeamsConvertToChannelProps({})); + }); + + it('should return true if teamName is provided', () => { + chai.assert.isTrue(isTeamsConvertToChannelProps({ teamName: 'teamName' })); + }); + + it('should return true if teamId is provided', () => { + chai.assert.isTrue(isTeamsConvertToChannelProps({ teamId: 'teamId' })); + }); + + it('should return false if both teamName and teamId are provided', () => { + chai.assert.isFalse(isTeamsConvertToChannelProps({ teamName: 'teamName', teamId: 'teamId' })); + }); + + it('should return false if teamName is not a string', () => { + chai.assert.isFalse(isTeamsConvertToChannelProps({ teamName: 1 })); + }); + + it('should return false if teamId is not a string', () => { + chai.assert.isFalse(isTeamsConvertToChannelProps({ teamId: 1 })); + }); + + it('should return false if an additionalProperties is provided', () => { + chai.assert.isFalse(isTeamsConvertToChannelProps({ teamName: 'teamName', additionalProperties: 'additionalProperties' })); + }); + }); +}); diff --git a/definition/rest/v1/teams/TeamsConvertToChannelProps.ts b/definition/rest/v1/teams/TeamsConvertToChannelProps.ts new file mode 100644 index 00000000000..af93a570209 --- /dev/null +++ b/definition/rest/v1/teams/TeamsConvertToChannelProps.ts @@ -0,0 +1,54 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + + +const ajv = new Ajv(); + +export type TeamsConvertToChannelProps = { + roomsToRemove?: string[]; +} & ({ teamId: string } | { teamName: string }); + +const teamsConvertToTeamsPropsSchema: JSONSchemaType = { + oneOf: [ + { + type: 'object', + + properties: { + roomsToRemove: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + teamId: { + type: 'string', + }, + }, + required: [ + 'teamId', + ], + additionalProperties: false, + }, + { + type: 'object', + properties: { + roomsToRemove: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + teamName: { + type: 'string', + }, + }, + required: [ + 'teamName', + ], + additionalProperties: false, + }, + ], +}; + +export const isTeamsConvertToChannelProps = ajv.compile(teamsConvertToTeamsPropsSchema); diff --git a/definition/rest/v1/teams/TeamsDeleteProps.test.ts b/definition/rest/v1/teams/TeamsDeleteProps.test.ts new file mode 100644 index 00000000000..9efc17cd1de --- /dev/null +++ b/definition/rest/v1/teams/TeamsDeleteProps.test.ts @@ -0,0 +1,64 @@ +/* eslint-env mocha */ +import chai from 'chai'; + +import { isTeamsDeleteProps } from './TeamsDeleteProps'; + +describe('TeamsDeleteProps (definition/rest/v1)', () => { + describe('isTeamsDeleteProps', () => { + it('should be a function', () => { + chai.assert.isFunction(isTeamsDeleteProps); + }); + + it('should return false if neither teamName or teamId is provided', () => { + chai.assert.isFalse(isTeamsDeleteProps({})); + }); + + it('should return true if teamId is provided', () => { + chai.assert.isTrue(isTeamsDeleteProps({ teamId: 'teamId' })); + }); + + it('should return true if teamName is provided', () => { + chai.assert.isTrue(isTeamsDeleteProps({ teamName: 'teamName' })); + }); + + it('should return false if teamId and roomsToRemove are provided, but roomsToRemove is empty', () => { + chai.assert.isFalse(isTeamsDeleteProps({ teamId: 'teamId', roomsToRemove: [] })); + }); + + it('should return false if teamName and roomsToRemove are provided, but roomsToRemove is empty', () => { + chai.assert.isFalse(isTeamsDeleteProps({ teamName: 'teamName', roomsToRemove: [] })); + }); + + it('should return true if teamId and roomsToRemove are provided', () => { + chai.assert.isTrue(isTeamsDeleteProps({ teamId: 'teamId', roomsToRemove: ['roomId'] })); + }); + + it('should return true if teamName and roomsToRemove are provided', () => { + chai.assert.isTrue(isTeamsDeleteProps({ teamName: 'teamName', roomsToRemove: ['roomId'] })); + }); + + it('should return false if teamId and roomsToRemove are provided, but roomsToRemove is not an array', () => { + chai.assert.isFalse(isTeamsDeleteProps({ teamId: 'teamId', roomsToRemove: {} })); + }); + + it('should return false if teamName and roomsToRemove are provided, but roomsToRemove is not an array', () => { + chai.assert.isFalse(isTeamsDeleteProps({ teamName: 'teamName', roomsToRemove: {} })); + }); + + it('should return false if teamId and roomsToRemove are provided, but roomsToRemove is not an array of strings', () => { + chai.assert.isFalse(isTeamsDeleteProps({ teamId: 'teamId', roomsToRemove: [1] })); + }); + + it('should return false if teamName and roomsToRemove are provided, but roomsToRemove is not an array of strings', () => { + chai.assert.isFalse(isTeamsDeleteProps({ teamName: 'teamName', roomsToRemove: [1] })); + }); + + it('should return false if teamName and rooms are provided but an extra property is provided', () => { + chai.assert.isFalse(isTeamsDeleteProps({ teamName: 'teamName', roomsToRemove: ['roomsToRemove'], extra: 'extra' })); + }); + + it('should return false if teamId and rooms are provided but an extra property is provided', () => { + chai.assert.isFalse(isTeamsDeleteProps({ teamId: 'teamId', roomsToRemove: ['roomsToRemove'], extra: 'extra' })); + }); + }); +}); diff --git a/definition/rest/v1/teams/TeamsDeleteProps.ts b/definition/rest/v1/teams/TeamsDeleteProps.ts new file mode 100644 index 00000000000..22582488d0a --- /dev/null +++ b/definition/rest/v1/teams/TeamsDeleteProps.ts @@ -0,0 +1,50 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +const ajv = new Ajv(); + +export type TeamsDeleteProps = ({ teamId: string } | { teamName: string }) & { roomsToRemove?: string[] }; + +const teamsDeletePropsSchema: JSONSchemaType = { + oneOf: [ + { + type: 'object', + properties: { + teamId: { + type: 'string', + }, + roomsToRemove: { + type: 'array', + items: { + type: 'string', + }, + minItems: 1, + uniqueItems: true, + nullable: true, + }, + }, + required: ['teamId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + teamName: { + type: 'string', + }, + roomsToRemove: { + type: 'array', + items: { + type: 'string', + }, + minItems: 1, + uniqueItems: true, + nullable: true, + }, + }, + required: ['teamName'], + additionalProperties: false, + }, + ], +}; + +export const isTeamsDeleteProps = ajv.compile(teamsDeletePropsSchema); diff --git a/definition/rest/v1/teams/TeamsLeaveProps.test.ts b/definition/rest/v1/teams/TeamsLeaveProps.test.ts new file mode 100644 index 00000000000..1b6255d9797 --- /dev/null +++ b/definition/rest/v1/teams/TeamsLeaveProps.test.ts @@ -0,0 +1,64 @@ +/* eslint-env mocha */ +import chai from 'chai'; + +import { isTeamsLeaveProps } from './TeamsLeaveProps'; + +describe('TeamsLeaveProps (definition/rest/v1)', () => { + describe('isTeamsLeaveProps', () => { + it('should be a function', () => { + chai.assert.isFunction(isTeamsLeaveProps); + }); + + it('should return false if neither teamName or teamId is provided', () => { + chai.assert.isFalse(isTeamsLeaveProps({})); + }); + + it('should return true if teamId is provided', () => { + chai.assert.isTrue(isTeamsLeaveProps({ teamId: 'teamId' })); + }); + + it('should return true if teamName is provided', () => { + chai.assert.isTrue(isTeamsLeaveProps({ teamName: 'teamName' })); + }); + + it('should return false if teamId and roomsToRemove are provided, but roomsToRemove is empty', () => { + chai.assert.isFalse(isTeamsLeaveProps({ teamId: 'teamId', rooms: [] })); + }); + + it('should return false if teamName and rooms are provided, but rooms is empty', () => { + chai.assert.isFalse(isTeamsLeaveProps({ teamName: 'teamName', rooms: [] })); + }); + + it('should return true if teamId and rooms are provided', () => { + chai.assert.isTrue(isTeamsLeaveProps({ teamId: 'teamId', rooms: ['roomId'] })); + }); + + it('should return true if teamName and rooms are provided', () => { + chai.assert.isTrue(isTeamsLeaveProps({ teamName: 'teamName', rooms: ['roomId'] })); + }); + + it('should return false if teamId and rooms are provided, but rooms is not an array', () => { + chai.assert.isFalse(isTeamsLeaveProps({ teamId: 'teamId', rooms: {} })); + }); + + it('should return false if teamName and rooms are provided, but rooms is not an array', () => { + chai.assert.isFalse(isTeamsLeaveProps({ teamName: 'teamName', rooms: {} })); + }); + + it('should return false if teamId and rooms are provided, but rooms is not an array of strings', () => { + chai.assert.isFalse(isTeamsLeaveProps({ teamId: 'teamId', rooms: [1] })); + }); + + it('should return false if teamName and rooms are provided, but rooms is not an array of strings', () => { + chai.assert.isFalse(isTeamsLeaveProps({ teamName: 'teamName', rooms: [1] })); + }); + + it('should return false if teamName and rooms are provided but an extra property is provided', () => { + chai.assert.isFalse(isTeamsLeaveProps({ teamName: 'teamName', rooms: ['rooms'], extra: 'extra' })); + }); + + it('should return false if teamId and rooms are provided but an extra property is provided', () => { + chai.assert.isFalse(isTeamsLeaveProps({ teamId: 'teamId', rooms: ['rooms'], extra: 'extra' })); + }); + }); +}); diff --git a/definition/rest/v1/teams/TeamsLeaveProps.ts b/definition/rest/v1/teams/TeamsLeaveProps.ts new file mode 100644 index 00000000000..ac526886237 --- /dev/null +++ b/definition/rest/v1/teams/TeamsLeaveProps.ts @@ -0,0 +1,51 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + + +const ajv = new Ajv(); + +export type TeamsLeaveProps = ({ teamId: string } | { teamName: string }) & { rooms?: string[] }; + +const teamsLeavePropsSchema: JSONSchemaType = { + oneOf: [ + { + type: 'object', + properties: { + teamId: { + type: 'string', + }, + rooms: { + type: 'array', + items: { + type: 'string', + }, + minItems: 1, + uniqueItems: true, + nullable: true, + }, + }, + required: ['teamId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + teamName: { + type: 'string', + }, + rooms: { + type: 'array', + items: { + type: 'string', + }, + minItems: 1, + uniqueItems: true, + nullable: true, + }, + }, + required: ['teamName'], + additionalProperties: false, + }, + ], +}; + +export const isTeamsLeaveProps = ajv.compile(teamsLeavePropsSchema); diff --git a/definition/rest/v1/teams/TeamsRemoveMemberProps.test.ts b/definition/rest/v1/teams/TeamsRemoveMemberProps.test.ts new file mode 100644 index 00000000000..f66c765e034 --- /dev/null +++ b/definition/rest/v1/teams/TeamsRemoveMemberProps.test.ts @@ -0,0 +1,61 @@ +/* eslint-env mocha */ +import chai from 'chai'; + +import { isTeamsRemoveMemberProps } from './TeamsRemoveMemberProps'; + +describe('Teams (definition/rest/v1)', () => { + describe('isTeamsRemoveMemberProps', () => { + it('should be a function', () => { + chai.assert.isFunction(isTeamsRemoveMemberProps); + }); + it('should return false if parameter is empty', () => { + chai.assert.isFalse(isTeamsRemoveMemberProps({})); + }); + it('should return false if teamId is is informed but missing userId', () => { + chai.assert.isFalse(isTeamsRemoveMemberProps({ teamId: 'teamId' })); + }); + it('should return false if teamName is is informed but missing userId', () => { + chai.assert.isFalse(isTeamsRemoveMemberProps({ teamName: 'teamName' })); + }); + + it('should return true if teamId and userId are informed', () => { + chai.assert.isTrue(isTeamsRemoveMemberProps({ teamId: 'teamId', userId: 'userId' })); + }); + it('should return true if teamName and userId are informed', () => { + chai.assert.isTrue(isTeamsRemoveMemberProps({ teamName: 'teamName', userId: 'userId' })); + }); + + + it('should return false if teamName and userId are informed but rooms are empty', () => { + chai.assert.isFalse(isTeamsRemoveMemberProps({ teamName: 'teamName', userId: 'userId', rooms: [] })); + }); + + it('should return false if teamId and userId are informed and rooms are empty', () => { + chai.assert.isFalse(isTeamsRemoveMemberProps({ teamId: 'teamId', userId: 'userId', rooms: [] })); + }); + + it('should return false if teamId and userId are informed but rooms are empty', () => { + chai.assert.isFalse(isTeamsRemoveMemberProps({ teamId: 'teamId', userId: 'userId', rooms: [] })); + }); + + it('should return true if teamId and userId are informed and rooms are informed', () => { + chai.assert.isTrue(isTeamsRemoveMemberProps({ teamId: 'teamId', userId: 'userId', rooms: ['room'] })); + }); + + it('should return false if teamId and userId are informed and rooms are informed but rooms is not an array of strings', () => { + chai.assert.isFalse(isTeamsRemoveMemberProps({ teamId: 'teamId', userId: 'userId', rooms: [123] })); + }); + + it('should return false if teamName and userId are informed and rooms are informed but there is an extra property', () => { + chai.assert.isFalse(isTeamsRemoveMemberProps({ teamName: 'teamName', userId: 'userId', rooms: ['room'], extra: 'extra' })); + }); + + it('should return false if teamId and userId are informed and rooms are informed but there is an extra property', () => { + chai.assert.isFalse(isTeamsRemoveMemberProps({ teamId: 'teamId', userId: 'userId', rooms: ['room'], extra: 'extra' })); + }); + + it('should return false if teamName and userId are informed and rooms are informed but there is an extra property', () => { + chai.assert.isFalse(isTeamsRemoveMemberProps({ teamName: 'teamName', userId: 'userId', rooms: ['room'], extra: 'extra' })); + }); + }); +}); diff --git a/definition/rest/v1/teams/TeamsRemoveMemberProps.ts b/definition/rest/v1/teams/TeamsRemoveMemberProps.ts new file mode 100644 index 00000000000..7518fd351c8 --- /dev/null +++ b/definition/rest/v1/teams/TeamsRemoveMemberProps.ts @@ -0,0 +1,56 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +const ajv = new Ajv(); + +export type TeamsRemoveMemberProps = ({ teamId: string } | { teamName: string }) & { userId: string; rooms?: Array }; + +const teamsRemoveMemberPropsSchema: JSONSchemaType = { + oneOf: [ + { + type: 'object', + properties: { + teamId: { + type: 'string', + }, + userId: { + type: 'string', + }, + rooms: { + type: 'array', + items: { + type: 'string', + }, + minItems: 1, + uniqueItems: true, + nullable: true, + }, + }, + required: ['teamId', 'userId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + teamName: { + type: 'string', + }, + userId: { + type: 'string', + }, + rooms: { + type: 'array', + items: { + type: 'string', + }, + minItems: 1, + uniqueItems: true, + nullable: true, + }, + }, + required: ['teamName', 'userId'], + additionalProperties: false, + }, + ], +}; + +export const isTeamsRemoveMemberProps = ajv.compile(teamsRemoveMemberPropsSchema); diff --git a/definition/rest/v1/teams/TeamsRemoveRoomProps.test.ts b/definition/rest/v1/teams/TeamsRemoveRoomProps.test.ts new file mode 100644 index 00000000000..226185dcc95 --- /dev/null +++ b/definition/rest/v1/teams/TeamsRemoveRoomProps.test.ts @@ -0,0 +1,27 @@ +/* eslint-env mocha */ +import chai from 'chai'; + +import { isTeamsRemoveRoomProps } from './TeamsRemoveRoomProps'; + +describe('TeamsRemoveRoomProps (definition/rest/v1)', () => { + describe('isTeamsRemoveRoomProps', () => { + it('should be a function', () => { + chai.assert.isFunction(isTeamsRemoveRoomProps); + }); + it('should return false if roomId is not provided', () => { + chai.assert.isFalse(isTeamsRemoveRoomProps({})); + }); + it('should return false if roomId is provided but no teamId or teamName were provided', () => { + chai.assert.isFalse(isTeamsRemoveRoomProps({ roomId: 'roomId' })); + }); + it('should return false if roomId is provided and teamId is provided', () => { + chai.assert.isTrue(isTeamsRemoveRoomProps({ roomId: 'roomId', teamId: 'teamId' })); + }); + it('should return true if roomId is provided and teamName is provided', () => { + chai.assert.isTrue(isTeamsRemoveRoomProps({ roomId: 'roomId', teamName: 'teamName' })); + }); + it('should return false if roomId and teamName are provided but an additional property is provided', () => { + chai.assert.isFalse(isTeamsRemoveRoomProps({ roomId: 'roomId', teamName: 'teamName', foo: 'bar' })); + }); + }); +}); diff --git a/definition/rest/v1/teams/TeamsRemoveRoomProps.ts b/definition/rest/v1/teams/TeamsRemoveRoomProps.ts new file mode 100644 index 00000000000..a41db553889 --- /dev/null +++ b/definition/rest/v1/teams/TeamsRemoveRoomProps.ts @@ -0,0 +1,40 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +import type { IRoom } from '../../../IRoom'; + +const ajv = new Ajv(); + +export type TeamsRemoveRoomProps = ({ teamId: string } | { teamName: string }) & { roomId: IRoom['_id'] }; + +export const teamsRemoveRoomPropsSchema: JSONSchemaType = { + oneOf: [ + { + type: 'object', + properties: { + teamId: { + type: 'string', + }, + roomId: { + type: 'string', + }, + }, + required: ['teamId', 'roomId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + teamName: { + type: 'string', + }, + roomId: { + type: 'string', + }, + }, + required: ['teamName', 'roomId'], + additionalProperties: false, + }, + ], +}; + +export const isTeamsRemoveRoomProps = ajv.compile(teamsRemoveRoomPropsSchema); diff --git a/definition/rest/v1/teams/TeamsUpdateMemberProps.test.ts b/definition/rest/v1/teams/TeamsUpdateMemberProps.test.ts new file mode 100644 index 00000000000..ed6a329772a --- /dev/null +++ b/definition/rest/v1/teams/TeamsUpdateMemberProps.test.ts @@ -0,0 +1,59 @@ +/* eslint-env mocha */ +import chai from 'chai'; + +import { isTeamsUpdateMemberProps } from './TeamsUpdateMemberProps'; + +describe('TeamsUpdateMemberProps (definition/rest/v1)', () => { + describe('isTeamsUpdateMemberProps', () => { + it('should be a function', () => { + chai.assert.isFunction(isTeamsUpdateMemberProps); + }); + it('should return false if the parameter is empty', () => { + chai.assert.isFalse(isTeamsUpdateMemberProps({})); + }); + + it('should return false if teamId is provided but no member was provided', () => { + chai.assert.isFalse(isTeamsUpdateMemberProps({ teamId: '123' })); + }); + + it('should return false if teamName is provided but no member was provided', () => { + chai.assert.isFalse(isTeamsUpdateMemberProps({ teamName: '123' })); + }); + + it('should return false if member is provided but no teamId or teamName were provided', () => { + chai.assert.isFalse(isTeamsUpdateMemberProps({ member: { userId: '123' } })); + }); + + it('should return false if member with role is provided but no teamId or teamName were provided', () => { + chai.assert.isFalse(isTeamsUpdateMemberProps({ member: { userId: '123', roles: ['123'] } })); + }); + + it('should return true if member is provided and teamId is provided', () => { + chai.assert.isTrue(isTeamsUpdateMemberProps({ member: { userId: '123' }, teamId: '123' })); + }); + + it('should return true if member is provided and teamName is provided', () => { + chai.assert.isTrue(isTeamsUpdateMemberProps({ member: { userId: '123' }, teamName: '123' })); + }); + + it('should return true if member with role is provided and teamId is provided', () => { + chai.assert.isTrue(isTeamsUpdateMemberProps({ member: { userId: '123', roles: ['123'] }, teamId: '123' })); + }); + + it('should return true if member with role is provided and teamName is provided', () => { + chai.assert.isTrue(isTeamsUpdateMemberProps({ member: { userId: '123', roles: ['123'] }, teamName: '123' })); + }); + + it('should return false if teamName was provided and member contains an invalid property', () => { + chai.assert.isFalse(isTeamsUpdateMemberProps({ member: { userId: '123', invalid: '123' }, teamName: '123' })); + }); + + it('should return false if teamId was provided and member contains an invalid property', () => { + chai.assert.isFalse(isTeamsUpdateMemberProps({ member: { userId: '123', invalid: '123' }, teamId: '123' })); + }); + + it('should return false if contains an invalid property', () => { + chai.assert.isFalse(isTeamsUpdateMemberProps({ member: { userId: '123', roles: ['123'] }, teamName: '123', invalid: true })); + }); + }); +}); diff --git a/definition/rest/v1/teams/TeamsUpdateMemberProps.ts b/definition/rest/v1/teams/TeamsUpdateMemberProps.ts new file mode 100644 index 00000000000..5a65fc6238f --- /dev/null +++ b/definition/rest/v1/teams/TeamsUpdateMemberProps.ts @@ -0,0 +1,68 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +import { ITeamMemberParams } from '../../../../server/sdk/types/ITeamService'; + +const ajv = new Ajv(); + +export type TeamsUpdateMemberProps = ({ teamId: string } | { teamName: string }) & { member: ITeamMemberParams }; + +const teamsUpdateMemberPropsSchema: JSONSchemaType = { + oneOf: [ + { + type: 'object', + properties: { + teamId: { + type: 'string', + }, + member: { + type: 'object', + properties: { + userId: { + type: 'string', + }, + roles: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + }, + required: ['userId'], + additionalProperties: false, + }, + }, + required: ['teamId', 'member'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + teamName: { + type: 'string', + }, + member: { + type: 'object', + properties: { + userId: { + type: 'string', + }, + roles: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + }, + required: ['userId'], + additionalProperties: false, + }, + }, + required: ['teamName', 'member'], + additionalProperties: false, + }, + ], +}; + +export const isTeamsUpdateMemberProps = ajv.compile(teamsUpdateMemberPropsSchema); diff --git a/definition/rest/v1/teams/TeamsUpdateProps.test.ts b/definition/rest/v1/teams/TeamsUpdateProps.test.ts new file mode 100644 index 00000000000..ce09985901d --- /dev/null +++ b/definition/rest/v1/teams/TeamsUpdateProps.test.ts @@ -0,0 +1,161 @@ +/* eslint-env mocha */ +import chai from 'chai'; + +import { isTeamsUpdateProps } from './TeamsUpdateProps'; + +describe('TeamsUpdateMemberProps (definition/rest/v1)', () => { + describe('isTeamsUpdateProps', () => { + it('should be a function', () => { + chai.assert.isFunction(isTeamsUpdateProps); + }); + it('should return false when provided anything that is not an TeamsUpdateProps', () => { + chai.assert.isFalse(isTeamsUpdateProps(undefined)); + chai.assert.isFalse(isTeamsUpdateProps(null)); + chai.assert.isFalse(isTeamsUpdateProps('')); + chai.assert.isFalse(isTeamsUpdateProps(123)); + chai.assert.isFalse(isTeamsUpdateProps({})); + chai.assert.isFalse(isTeamsUpdateProps([])); + chai.assert.isFalse(isTeamsUpdateProps(new Date())); + chai.assert.isFalse(isTeamsUpdateProps(new Error())); + }); + it('should return false when only teamName is provided to TeamsUpdateProps', () => { + chai.assert.isFalse(isTeamsUpdateProps({ + teamName: 'teamName', + })); + }); + + it('should return false when only teamId is provided to TeamsUpdateProps', () => { + chai.assert.isFalse(isTeamsUpdateProps({ + teamId: 'teamId', + })); + }); + + it('should return false when teamName and data are provided to TeamsUpdateProps but data is an empty object', () => { + chai.assert.isFalse(isTeamsUpdateProps({ + teamName: 'teamName', + data: {}, + })); + }); + + it('should return false when teamId and data are provided to TeamsUpdateProps but data is an empty object', () => { + chai.assert.isFalse(isTeamsUpdateProps({ + teamId: 'teamId', + data: {}, + })); + }); + + it('should return false when teamName and data are provided to TeamsUpdateProps but data is not an object', () => { + chai.assert.isFalse(isTeamsUpdateProps({ + teamName: 'teamName', + data: 'data', + })); + }); + + it('should return false when teamId and data are provided to TeamsUpdateProps but data is not an object', () => { + chai.assert.isFalse(isTeamsUpdateProps({ + teamId: 'teamId', + data: 'data', + })); + }); + + it('should return true when teamName and data.name are provided to TeamsUpdateProps', () => { + chai.assert.isTrue(isTeamsUpdateProps({ + teamName: 'teamName', + data: { + name: 'name', + }, + })); + }); + + it('should return true when teamId and data.name are provided to TeamsUpdateProps', () => { + chai.assert.isTrue(isTeamsUpdateProps({ + teamId: 'teamId', + data: { + name: 'name', + }, + })); + }); + + it('should return true when teamName and data.type are provided to TeamsUpdateProps', () => { + chai.assert.isTrue(isTeamsUpdateProps({ + teamName: 'teamName', + data: { + type: 0, + }, + })); + }); + + it('should return true when teamId and data.type are provided to TeamsUpdateProps', () => { + chai.assert.isTrue(isTeamsUpdateProps({ + teamId: 'teamId', + data: { + type: 0, + }, + })); + }); + + it('should return true when teamName and data.name and data.type are provided to TeamsUpdateProps', () => { + chai.assert.isTrue(isTeamsUpdateProps({ + teamName: 'teamName', + data: { + name: 'name', + type: 0, + }, + })); + }); + + it('should return true when teamId and data.name and data.type are provided to TeamsUpdateProps', () => { + chai.assert.isTrue(isTeamsUpdateProps({ + teamId: 'teamId', + data: { + name: 'name', + type: 0, + }, + })); + }); + + it('should return false when teamName, data.name, data.type are some more extra data are provided to TeamsUpdateProps', () => { + chai.assert.isFalse(isTeamsUpdateProps({ + teamName: 'teamName', + data: { + name: 'name', + type: 0, + extra: 'extra', + }, + })); + }); + + it('should return false when teamId, data.name, data.type are some more extra data are provided to TeamsUpdateProps', () => { + chai.assert.isFalse(isTeamsUpdateProps({ + teamId: 'teamId', + data: { + name: 'name', + type: 0, + extra: 'extra', + }, + })); + }); + + it('should return false when teamName, data.name, data.type are some more extra parameter are provided to TeamsUpdateProps', () => { + chai.assert.isFalse(isTeamsUpdateProps({ + teamName: 'teamName', + extra: 'extra', + data: { + name: 'name', + type: 0, + }, + })); + }); + + it('should return false when teamId, data.name, data.type are some more extra parameter are provided to TeamsUpdateProps', () => { + chai.assert.isFalse(isTeamsUpdateProps({ + teamId: 'teamId', + extra: 'extra', + data: { + name: 'name', + type: 0, + }, + })); + }); + }); +}); diff --git a/definition/rest/v1/teams/TeamsUpdateProps.ts b/definition/rest/v1/teams/TeamsUpdateProps.ts new file mode 100644 index 00000000000..b019efdb154 --- /dev/null +++ b/definition/rest/v1/teams/TeamsUpdateProps.ts @@ -0,0 +1,75 @@ +import Ajv, { JSONSchemaType } from 'ajv'; + +import { TEAM_TYPE } from '../../../ITeam'; + +const ajv = new Ajv(); + +export type TeamsUpdateProps = ({ teamId: string } | { teamName: string }) & { + data: ({ + name: string; + type?: TEAM_TYPE; + } | { + name?: string; + type: TEAM_TYPE; + }); +}; + +const teamsUpdatePropsSchema: JSONSchemaType = { + type: 'object', + properties: { + updateRoom: { + type: 'boolean', + nullable: true, + }, + teamId: { + type: 'string', + nullable: true, + }, + teamName: { + type: 'string', + nullable: true, + }, + data: { + type: 'object', + properties: { + name: { + type: 'string', + nullable: true, + }, + type: { + type: 'number', + enum: [ + TEAM_TYPE.PUBLIC, + TEAM_TYPE.PRIVATE, + ], + }, + }, + additionalProperties: false, + required: [], + anyOf: [ + { + required: ['name'], + }, + { + required: ['type'], + }, + ], + }, + name: { + type: 'string', + nullable: true, + }, + }, + required: [], + oneOf: [ + { + required: ['teamId', 'data'], + }, + { + required: ['teamName', 'data'], + }, + ], + additionalProperties: false, +}; + +export const isTeamsUpdateProps = ajv.compile(teamsUpdatePropsSchema); diff --git a/definition/rest/v1/teams/index.ts b/definition/rest/v1/teams/index.ts new file mode 100644 index 00000000000..72694da7ea0 --- /dev/null +++ b/definition/rest/v1/teams/index.ts @@ -0,0 +1,148 @@ + + +import type { IRoom } from '../../../IRoom'; +import type { ITeam } from '../../../ITeam'; +import type { IUser } from '../../../IUser'; +import { PaginatedResult } from '../../helpers/PaginatedResult'; +import { PaginatedRequest } from '../../helpers/PaginatedRequest'; +import { ITeamAutocompleteResult, ITeamMemberInfo } from '../../../../server/sdk/types/ITeamService'; +import { TeamsRemoveRoomProps } from './TeamsRemoveRoomProps'; +import { TeamsConvertToChannelProps } from './TeamsConvertToChannelProps'; +import { TeamsUpdateMemberProps } from './TeamsUpdateMemberProps'; +import { TeamsAddMembersProps } from './TeamsAddMembersProps'; +import { TeamsRemoveMemberProps } from './TeamsRemoveMemberProps'; +import { TeamsDeleteProps } from './TeamsDeleteProps'; +import { TeamsLeaveProps } from './TeamsLeaveProps'; +import { TeamsUpdateProps } from './TeamsUpdateProps'; + + +type TeamProps = + | TeamsRemoveRoomProps + | TeamsConvertToChannelProps + | TeamsUpdateMemberProps + | TeamsAddMembersProps + | TeamsRemoveMemberProps + | TeamsDeleteProps + | TeamsLeaveProps + | TeamsUpdateProps; + +export const isTeamPropsWithTeamName = (props: T): props is T & { teamName: string } => 'teamName' in props; + +export const isTeamPropsWithTeamId = (props: T): props is T & { teamId: string } => 'teamId' in props; + +export type TeamsEndpoints = { + 'teams.list': { + GET: () => PaginatedResult & { teams: ITeam[] }; + }; + 'teams.listAll': { + GET: () => { teams: ITeam[] } & PaginatedResult; + }; + 'teams.create': { + POST: (params: { + name: ITeam['name']; + type?: ITeam['type']; + members?: IUser['_id'][]; + room: { + id?: string; + name?: IRoom['name']; + members?: IUser['_id'][]; + readOnly?: boolean; + extraData?: { + teamId?: string; + teamMain?: boolean; + } & { [key: string]: string | boolean }; + options?: { + nameValidationRegex?: string; + creator: string; + subscriptionExtra?: { + open: boolean; + ls: Date; + prid: IRoom['_id']; + }; + } & { + [key: string]: + | string + | { + open: boolean; + ls: Date; + prid: IRoom['_id']; + }; + }; + }; + owner?: IUser['_id']; + }) => { + team: ITeam; + }; + }; + + 'teams.convertToChannel': { + POST: (params: TeamsConvertToChannelProps) => void; + }; + + 'teams.addRooms': { + POST: (params: { rooms: IRoom['_id'][]; teamId: string } | { rooms: IRoom['_id'][]; teamName: string }) => ({ rooms: IRoom[] }); + }; + + 'teams.removeRoom': { + POST: (params: TeamsRemoveRoomProps) => ({ room: IRoom }); + }; + + 'teams.members': { + GET: (params: ({ teamId: string } | { teamName: string }) & { status?: string[]; username?: string; name?: string }) => (PaginatedResult & { members: ITeamMemberInfo[] }); + }; + + 'teams.addMembers': { + POST: (params: TeamsAddMembersProps) => void; + }; + + 'teams.updateMember': { + POST: (params: TeamsUpdateMemberProps) => void; + }; + + 'teams.removeMember': { + POST: (params: TeamsRemoveMemberProps) => void; + }; + + 'teams.leave': { + POST: (params: TeamsLeaveProps) => void; + }; + + + 'teams.info': { + GET: (params: ({ teamId: string } | { teamName: string }) & {}) => ({ teamInfo: Partial }); + }; + + 'teams.autocomplete': { + GET: (params: { name: string }) => ({ teams: ITeamAutocompleteResult[] }); + }; + + 'teams.update': { + POST: (params: TeamsUpdateProps) => void; + }; + + 'teams.delete': { + POST: (params: TeamsDeleteProps) => void; + }; + + 'teams.listRoomsOfUser': { + GET: (params: { + teamId: ITeam['_id']; + userId: IUser['_id']; + canUserDelete?: boolean; + } | { + teamName: ITeam['name']; + userId: IUser['_id']; + canUserDelete?: boolean; + } + ) => PaginatedResult & { rooms: IRoom[] }; + }; + + 'teams.listRooms': { + GET: (params: PaginatedRequest & ({ teamId: string } | { teamId: string }) & { filter?: string; type?: string }) => PaginatedResult & { rooms: IRoom[] }; + }; + + + 'teams.updateRoom': { + POST: (params: { roomId: IRoom['_id']; isDefault: boolean }) => ({ room: IRoom }); + }; +}; diff --git a/client/contexts/ServerContext/endpoints/v1/users.ts b/definition/rest/v1/users.ts similarity index 71% rename from client/contexts/ServerContext/endpoints/v1/users.ts rename to definition/rest/v1/users.ts index cbe8a74c8d4..337a2182f2e 100644 --- a/client/contexts/ServerContext/endpoints/v1/users.ts +++ b/definition/rest/v1/users.ts @@ -1,5 +1,5 @@ -import type { ITeam } from '../../../../../definition/ITeam'; -import type { IUser } from '../../../../../definition/IUser'; +import type { ITeam } from '../../ITeam'; +import type { IUser } from '../../IUser'; export type UsersEndpoints = { 'users.2fa.sendEmailCode': { diff --git a/definition/utils.ts b/definition/utils.ts index 90e7f59df57..72339afa798 100644 --- a/definition/utils.ts +++ b/definition/utils.ts @@ -7,3 +7,5 @@ export type ValueOf = T[keyof T]; export type UnionToIntersection = (T extends any ? (x: T) => void : never) extends (x: infer U) => void ? U : never; + +export type Awaited = T extends PromiseLike ? Awaited : T; diff --git a/ee/app/license/definitions/ILicense.ts b/ee/app/license/definitions/ILicense.ts new file mode 100644 index 00000000000..014912bef1a --- /dev/null +++ b/ee/app/license/definitions/ILicense.ts @@ -0,0 +1,11 @@ +import { ILicenseTag } from './ILicenseTag'; + +export interface ILicense { + url: string; + expiry: string; + maxActiveUsers: number; + modules: string[]; + maxGuestUsers: number; + maxRoomsPerGuest: number; + tag?: ILicenseTag; +} diff --git a/ee/app/license/definitions/ILicenseTag.ts b/ee/app/license/definitions/ILicenseTag.ts new file mode 100644 index 00000000000..2f11fdebd5d --- /dev/null +++ b/ee/app/license/definitions/ILicenseTag.ts @@ -0,0 +1,4 @@ +export interface ILicenseTag { + name: string; + color: string; +} diff --git a/ee/app/license/server/license.ts b/ee/app/license/server/license.ts index ad0b962759f..dd666753f62 100644 --- a/ee/app/license/server/license.ts +++ b/ee/app/license/server/license.ts @@ -4,24 +4,11 @@ import { Users } from '../../../../app/models/server'; import { getBundleModules, isBundle, getBundleFromModule } from './bundles'; import decrypt from './decrypt'; import { getTagColor } from './getTagColor'; +import { ILicense } from '../definitions/ILicense'; +import { ILicenseTag } from '../definitions/ILicenseTag'; const EnterpriseLicenses = new EventEmitter(); -interface ILicenseTag { - name: string; - color: string; -} - -export interface ILicense { - url: string; - expiry: string; - maxActiveUsers: number; - modules: string[]; - maxGuestUsers: number; - maxRoomsPerGuest: number; - tag?: ILicenseTag; -} - export interface IValidLicense { valid?: boolean; license: ILicense; diff --git a/ee/app/livechat-enterprise/server/api/business-hours.ts b/ee/app/livechat-enterprise/server/api/business-hours.ts index aab6a58bb40..4640ad58ae3 100644 --- a/ee/app/livechat-enterprise/server/api/business-hours.ts +++ b/ee/app/livechat-enterprise/server/api/business-hours.ts @@ -1,21 +1,20 @@ import { API } from '../../../../../app/api/server'; import { findBusinessHours } from '../business-hour/lib/business-hour'; -// @ts-ignore API.v1.addRoute('livechat/business-hours.list', { authRequired: true }, { - get() { + async get() { const { offset, count } = this.getPaginationItems(); const { sort } = this.parseJsonQuery(); const { name } = this.queryParams; // @ts-ignore - return API.v1.success(Promise.await(findBusinessHours( + return API.v1.success(await findBusinessHours( this.userId, { offset, count, sort, }, - name))); + name)); }, }); diff --git a/ee/app/livechat-enterprise/server/business-hour/Helper.ts b/ee/app/livechat-enterprise/server/business-hour/Helper.ts index ef729e8def2..dd8da629368 100644 --- a/ee/app/livechat-enterprise/server/business-hour/Helper.ts +++ b/ee/app/livechat-enterprise/server/business-hour/Helper.ts @@ -10,7 +10,7 @@ import { import { ILivechatBusinessHour, LivechatBusinessHourTypes } from '../../../../../definition/ILivechatBusinessHour'; const getAllAgentIdsWithoutDepartment = async (): Promise => { - const agentIdsWithDepartment = (await LivechatDepartmentAgents.find({}, { fields: { agentId: 1 } }).toArray()).map((dept: any) => dept.agentId); + const agentIdsWithDepartment = (await LivechatDepartmentAgents.find({}, { projection: { agentId: 1 } }).toArray()).map((dept: any) => dept.agentId); const agentIdsWithoutDepartment = (await Users.findUsersInRolesWithQuery('livechat-agent', { _id: { $nin: agentIdsWithDepartment }, }, { projection: { _id: 1 } }).toArray()).map((user: any) => user._id); diff --git a/ee/client/contexts/ServerContext/endpoints/v1/engagementDashboard.ts b/ee/client/contexts/ServerContext/endpoints/v1/engagementDashboard.ts deleted file mode 100644 index 1365da57e7f..00000000000 --- a/ee/client/contexts/ServerContext/endpoints/v1/engagementDashboard.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { IDailyActiveUsers } from '../../../../../../definition/IUser'; -import { Serialized } from '../../../../../../definition/Serialized'; - -export type EngagementDashboardEndpoints = { - 'engagement-dashboard/users/active-users': { - GET: (params: { start: string; end: string }) => { - month: Serialized[]; - }; - }; -}; diff --git a/ee/definition/rest/index.ts b/ee/definition/rest/index.ts new file mode 100644 index 00000000000..292ebd766aa --- /dev/null +++ b/ee/definition/rest/index.ts @@ -0,0 +1,4 @@ +import type { EngagementDashboardEndpoints } from './v1/engagementDashboard'; +import type { OmnichannelBusinessHoursEndpoints } from './v1/omnichannel/businessHours'; + +export type EnterpriseEndpoints = EngagementDashboardEndpoints & OmnichannelBusinessHoursEndpoints; diff --git a/ee/definition/rest/v1/engagementDashboard.ts b/ee/definition/rest/v1/engagementDashboard.ts new file mode 100644 index 00000000000..f20ab3307f6 --- /dev/null +++ b/ee/definition/rest/v1/engagementDashboard.ts @@ -0,0 +1,62 @@ +import { IDirectMessageRoom, IRoom } from '../../../../definition/IRoom'; +import { IDailyActiveUsers } from '../../../../definition/IUser'; +import { Serialized } from '../../../../definition/Serialized'; + +export type EngagementDashboardEndpoints = { + '/v1/engagement-dashboard/channels/list': { + GET: (params: { start: Date; end: Date; offset: number; count: number }) => { + channels: { + room: { + _id: IRoom['_id']; + name: IRoom['name'] | IRoom['fname']; + ts: IRoom['ts']; + t: IRoom['t']; + _updatedAt: IRoom['_updatedAt']; + usernames?: IDirectMessageRoom['usernames']; + }; + messages: number; + lastWeekMessages: number; + diffFromLastWeek: number; + }[]; + count: number; + offset: number; + total: number; + }; + }; + 'engagement-dashboard/messages/origin': { + GET: (params: { start: Date; end: Date }) => { + origins: { + t: IRoom['t']; + messages: number; + }[]; + }; + }; + 'engagement-dashboard/messages/top-five-popular-channels': { + GET: (params: { start: Date; end: Date }) => { + channels: { + t: IRoom['t']; + messages: number; + name: IRoom['name'] | IRoom['fname']; + usernames?: IDirectMessageRoom['usernames']; + }[]; + }; + }; + 'engagement-dashboard/messages/messages-sent': { + GET: (params: { start: Date; end: Date }) => { + days: { day: Date; messages: number }[]; + period: { + count: number; + variation: number; + }; + yesterday: { + count: number; + variation: number; + }; + }; + }; + 'engagement-dashboard/users/active-users': { + GET: (params: { start: string; end: string }) => { + month: Serialized[]; + }; + }; +}; diff --git a/ee/definition/rest/v1/omnichannel/businessHours.ts b/ee/definition/rest/v1/omnichannel/businessHours.ts new file mode 100644 index 00000000000..77b352c6125 --- /dev/null +++ b/ee/definition/rest/v1/omnichannel/businessHours.ts @@ -0,0 +1,7 @@ +import { ILivechatBusinessHour } from '../../../../../definition/ILivechatBusinessHour'; + +export type OmnichannelBusinessHoursEndpoints = { + 'livechat/business-hours.list': { + GET: () => ({ businessHours: ILivechatBusinessHour[] }); + }; +} diff --git a/ee/server/api/ldap.ts b/ee/server/api/ldap.ts index 1c4627e585d..c51d21e588a 100644 --- a/ee/server/api/ldap.ts +++ b/ee/server/api/ldap.ts @@ -5,7 +5,7 @@ import { LDAPEE } from '../sdk'; import { hasLicense } from '../../app/license/server/license'; API.v1.addRoute('ldap.syncNow', { authRequired: true }, { - post() { + async post() { if (!this.userId) { throw new Error('error-invalid-user'); } @@ -22,10 +22,10 @@ API.v1.addRoute('ldap.syncNow', { authRequired: true }, { throw new Error('LDAP_disabled'); } - LDAPEE.sync(); + await LDAPEE.sync(); return API.v1.success({ - message: 'Sync_in_progress', + message: 'Sync_in_progress' as const, }); }, }); diff --git a/ee/server/api/licenses.ts b/ee/server/api/licenses.ts index 0972584c398..c59ab4e4060 100644 --- a/ee/server/api/licenses.ts +++ b/ee/server/api/licenses.ts @@ -1,9 +1,10 @@ import { check } from 'meteor/check'; -import { ILicense, getLicenses, validateFormat, flatModules, getMaxActiveUsers } from '../../app/license/server/license'; +import { getLicenses, validateFormat, flatModules, getMaxActiveUsers } from '../../app/license/server/license'; import { Settings, Users } from '../../../app/models/server'; import { API } from '../../../app/api/server/api'; import { hasPermission } from '../../../app/authorization/server'; +import { ILicense } from '../../app/license/definitions/ILicense'; function licenseTransform(license: ILicense): ILicense { return { diff --git a/ee/server/index.js b/ee/server/index.ts similarity index 100% rename from ee/server/index.js rename to ee/server/index.ts diff --git a/ee/server/lib/ldap/Manager.ts b/ee/server/lib/ldap/Manager.ts index 6b1c46b34ee..b146244a185 100644 --- a/ee/server/lib/ldap/Manager.ts +++ b/ee/server/lib/ldap/Manager.ts @@ -192,7 +192,7 @@ export class LDAPEEManager extends LDAPManager { } const roles = await Roles.find({}, { - fields: { + projection: { _updatedAt: 0, }, }).toArray() as Array; diff --git a/ee/server/services/package-lock.json b/ee/server/services/package-lock.json index 3b1cfe814ec..110aa6323e6 100644 --- a/ee/server/services/package-lock.json +++ b/ee/server/services/package-lock.json @@ -419,6 +419,17 @@ "debug": "4" } }, + "ajv": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.7.1.tgz", + "integrity": "sha512-gPpOObTO1QjbnN1sVMjJcp1TF9nggMfO4MBR5uQl6ZVTOaEPq5i4oq/6R9q2alMMPB3eg53wFv1RuJBLuxf3Hw==", + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, "amp": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/amp/-/amp-0.3.1.tgz", @@ -1225,6 +1236,11 @@ } } }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, "fast-json-patch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.0.tgz", @@ -1676,6 +1692,11 @@ "pako": "^0.2.5" } }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -2490,6 +2511,11 @@ "once": "^1.3.1" } }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", @@ -2564,6 +2590,11 @@ "ttl": "^1.3.0" } }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" + }, "require-in-the-middle": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-5.1.0.tgz", @@ -3070,6 +3101,14 @@ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/ee/server/services/package.json b/ee/server/services/package.json index 5000e768d23..984e7ce794b 100644 --- a/ee/server/services/package.json +++ b/ee/server/services/package.json @@ -19,6 +19,7 @@ "license": "MIT", "dependencies": { "@rocket.chat/string-helpers": "^0.29.0", + "ajv": "^8.7.1", "bcrypt": "^5.0.1", "body-parser": "^1.19.0", "colorette": "^1.3.0", diff --git a/package-lock.json b/package-lock.json index fb1db4f80cf..7aa70cb8990 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9525,6 +9525,12 @@ "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, + "@types/cookiejar": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.2.tgz", + "integrity": "sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==", + "dev": true + }, "@types/dompurify": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.2.2.tgz", @@ -10167,6 +10173,25 @@ "integrity": "sha512-+mdBIb+pxJ9SLwtjc2DgolMm8U7CG6qBdCevkjSsFB7ehJ0EExFd2ltKQ6m9CoKitqXwe6Tx5h+fAcklGQD0Bw==", "dev": true }, + "@types/superagent": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.13.tgz", + "integrity": "sha512-YIGelp3ZyMiH0/A09PMAORO0EBGlF5xIKfDpK74wdYvWUs2o96b5CItJcWPdH409b7SAXIIG6p8NdU/4U2Maww==", + "dev": true, + "requires": { + "@types/cookiejar": "*", + "@types/node": "*" + } + }, + "@types/supertest": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-2.0.11.tgz", + "integrity": "sha512-uci4Esokrw9qGb9bvhhSVEjd6rkny/dk5PK/Qz4yxKiyppEI+dOPlNrZBahE3i+PoKFYyDxChVXZ/ysS/nrm1Q==", + "dev": true, + "requires": { + "@types/superagent": "*" + } + }, "@types/tapable": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", @@ -11097,14 +11122,21 @@ } }, "ajv": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.6.1.tgz", - "integrity": "sha512-ZoJjft5B+EJBjUyu9C9Hc0OZyPZSSlOF+plzouTrg6UlA8f+e/n8NIgBFG/9tppJtpPWfthHakK7juJdNDODww==", + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.7.1.tgz", + "integrity": "sha512-gPpOObTO1QjbnN1sVMjJcp1TF9nggMfO4MBR5uQl6ZVTOaEPq5i4oq/6R9q2alMMPB3eg53wFv1RuJBLuxf3Hw==", "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", "uri-js": "^4.2.2" + }, + "dependencies": { + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + } } }, "ajv-errors": { @@ -19119,9 +19151,9 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-diff": { "version": "1.2.0", @@ -21771,6 +21803,19 @@ "requires": { "ajv": "^6.5.5", "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + } } }, "hard-rejection": { @@ -32395,8 +32440,7 @@ "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" }, "require_optional": { "version": "1.0.1", @@ -35390,6 +35434,20 @@ "ajv": "^6.1.0", "ajv-errors": "^1.0.0", "ajv-keywords": "^3.1.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + } } }, "source-map": { diff --git a/package.json b/package.json index c5865032264..38665a2c4d9 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "@types/semver": "^7.3.6", "@types/sharp": "^0.28.3", "@types/string-strip-html": "^5.0.0", + "@types/supertest": "^2.0.11", "@types/toastr": "^2.1.38", "@types/underscore.string": "0.0.38", "@types/use-subscription": "^1.0.0", @@ -182,6 +183,7 @@ "@types/lodash.debounce": "^4.0.6", "adm-zip": "0.4.14", "agenda": "github:RocketChat/agenda#3.1.2", + "ajv": "^8.7.1", "apn": "2.2.0", "archiver": "^3.1.1", "atlassian-crowd-patched": "^0.5.1", diff --git a/server/sdk/types/ITeamService.ts b/server/sdk/types/ITeamService.ts index 50396cd2055..697a67e4064 100644 --- a/server/sdk/types/ITeamService.ts +++ b/server/sdk/types/ITeamService.ts @@ -47,11 +47,14 @@ export interface IListRoomsFilter { allowPrivateTeam: boolean; } -export interface ITeamUpdateData { - name: string; - type: TEAM_TYPE; - updateRoom?: boolean; // default is true -} +export type ITeamUpdateData = + { updateRoom?: boolean } & ({ + name: string; + type?: TEAM_TYPE; + } | { + name?: string; + type: TEAM_TYPE; + }) export type ITeamAutocompleteResult = Pick; diff --git a/tests/end-to-end/api/13-roles.js b/tests/end-to-end/api/13-roles.js deleted file mode 100644 index 162152a0721..00000000000 --- a/tests/end-to-end/api/13-roles.js +++ /dev/null @@ -1,440 +0,0 @@ -import { expect } from 'chai'; - -import { - getCredentials, - api, - request, - credentials, - login, - apiRoleNameUsers, - apiRoleNameSubscriptions, - apiRoleScopeUsers, - apiRoleDescription, - apiRoleScopeSubscriptions, -} from '../../data/api-data.js'; -import { password } from '../../data/user'; -import { updatePermission } from '../../data/permissions.helper'; -import { createUser, login as doLogin } from '../../data/users.helper'; - -function createRole(name, scope, description) { - return new Promise((resolve) => { - request.post(api('roles.create')) - .set(credentials) - .send({ - name, - scope, - description, - }) - .end((err, req) => { - resolve(req.body.role); - }); - }); -} - -function addUserToRole(roleName, username, scope) { - return new Promise((resolve) => { - request.post(api('roles.addUserToRole')) - .set(credentials) - .send({ - roleName, - username, - roomId: scope, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end((err, req) => { - resolve(req.body.role); - }); - }); -} - -describe('[Roles]', function() { - this.retries(0); - - before((done) => getCredentials(done)); - - describe('GET [/roles.list]', () => { - it('should return all roles', (done) => { - request.get(api('roles.list')) - .set(credentials) - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('roles').and.to.be.an('array'); - }) - .end(done); - }); - }); - - describe('GET [/roles.sync]', () => { - it('should return an array of roles which are updated after updatedSice date when search by "updatedSince" query parameter', (done) => { - request.get(api('roles.sync?updatedSince=2018-11-27T13:52:01Z')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('roles'); - expect(res.body.roles).to.have.property('update').and.to.be.an('array'); - expect(res.body.roles).to.have.property('remove').and.to.be.an('array'); - }) - .end(done); - }); - - it('should return an error when updatedSince query parameter is not a valid ISODate string', (done) => { - request.get(api('roles.sync?updatedSince=fsafdf')) - .set(credentials) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - }) - .end(done); - }); - }); - - describe('POST [/roles.create]', () => { - it('should create a new role with Users scope', (done) => { - request.post(api('roles.create')) - .set(credentials) - .send({ - name: apiRoleNameUsers, - description: apiRoleDescription, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('role._id'); - expect(res.body).to.have.nested.property('role.name', apiRoleNameUsers); - expect(res.body).to.have.nested.property('role.scope', apiRoleScopeUsers); - expect(res.body).to.have.nested.property('role.description', apiRoleDescription); - }) - .end(done); - }); - - it('should create a new role with Subscriptions scope', (done) => { - request.post(api('roles.create')) - .set(credentials) - .send({ - name: apiRoleNameSubscriptions, - scope: apiRoleScopeSubscriptions, - description: apiRoleDescription, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('role._id'); - expect(res.body).to.have.nested.property('role.name', apiRoleNameSubscriptions); - expect(res.body).to.have.nested.property('role.scope', apiRoleScopeSubscriptions); - expect(res.body).to.have.nested.property('role.description', apiRoleDescription); - }) - .end(done); - }); - - it('should NOT create a new role with an existing role name', (done) => { - request.post(api('roles.create')) - .set(credentials) - .send({ - name: apiRoleNameUsers, - description: apiRoleDescription, - }) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.nested.property('error', 'Role name already exists [error-duplicate-role-names-not-allowed]'); - }) - .end(done); - }); - }); - - describe('POST [/roles.addUserToRole]', () => { - it('should assign a role with User scope to an user', (done) => { - request.post(api('roles.addUserToRole')) - .set(credentials) - .send({ - roleName: apiRoleNameUsers, - username: login.user, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('role._id'); - expect(res.body).to.have.nested.property('role.name', apiRoleNameUsers); - expect(res.body).to.have.nested.property('role.scope', apiRoleScopeUsers); - }) - .end(done); - }); - - it('should assign a role with Subscriptions scope to an user', (done) => { - request.post(api('roles.addUserToRole')) - .set(credentials) - .send({ - roleName: apiRoleNameSubscriptions, - username: login.user, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('role._id'); - expect(res.body).to.have.nested.property('role.name', apiRoleNameSubscriptions); - expect(res.body).to.have.nested.property('role.scope', apiRoleScopeSubscriptions); - }) - .end(done); - }); - }); - - describe('GET [/roles.getUsersInRole]', () => { - let userCredentials; - before((done) => { - createUser().then((createdUser) => { - doLogin(createdUser.username, password).then((createdUserCredentials) => { - userCredentials = createdUserCredentials; - updatePermission('access-permissions', ['admin', 'user']).then(done); - }); - }); - }); - it('should return an error when "role" query param is not provided', (done) => { - request.get(api('roles.getUsersInRole')) - .set(userCredentials) - .query({ - }) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body.errorType).to.be.equal('error-param-not-provided'); - }) - .end(done); - }); - it('should return an error when the user does not the necessary permission', (done) => { - updatePermission('access-permissions', ['admin']).then(() => { - request.get(api('roles.getUsersInRole')) - .set(userCredentials) - .query({ - role: 'admin', - }) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body.errorType).to.be.equal('error-not-allowed'); - }) - .end(done); - }); - }); - it('should return an error when the user try access rooms permissions and does not have the necessary permission', (done) => { - updatePermission('access-permissions', ['admin', 'user']).then(() => { - updatePermission('view-other-user-channels', []).then(() => { - request.get(api('roles.getUsersInRole')) - .set(userCredentials) - .query({ - role: 'admin', - roomId: 'GENERAL', - }) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body.errorType).to.be.equal('error-not-allowed'); - }) - .end(done); - }); - }); - }); - it('should return the list of users', (done) => { - updatePermission('access-permissions', ['admin', 'user']).then(() => { - updatePermission('view-other-user-channels', ['admin', 'user']).then(() => { - request.get(api('roles.getUsersInRole')) - .set(userCredentials) - .query({ - role: 'admin', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body.users).to.be.an('array'); - }) - .end(done); - }); - }); - }); - it('should return the list of users when find by room Id', (done) => { - request.get(api('roles.getUsersInRole')) - .set(userCredentials) - .query({ - role: 'admin', - roomId: 'GENERAL', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body.users).to.be.an('array'); - }) - .end(done); - }); - }); - - describe('POST [/roles.update]', () => { - const roleName = `role-${ Date.now() }`; - let newRole; - before(async () => { - newRole = await createRole(roleName, 'Users', 'Role description test'); - }); - - it('should update an existing role', (done) => { - const newRoleName = `${ roleName }Updated`; - const newRoleDescription = 'New role description'; - - request.post(api('roles.update')) - .set(credentials) - .send({ - roleId: newRole._id, - name: newRoleName, - description: newRoleDescription, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('role._id', newRole._id); - expect(res.body).to.have.nested.property('role.name', newRoleName); - expect(res.body).to.have.nested.property('role.scope', newRole.scope); - expect(res.body).to.have.nested.property('role.description', newRoleDescription); - }) - .end(done); - }); - - it('should NOT update a role with an existing role name', (done) => { - request.post(api('roles.update')) - .set(credentials) - .send({ - roleId: newRole._id, - name: apiRoleNameUsers, - }) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.nested.property('error', 'Role name already exists [error-duplicate-role-names-not-allowed]'); - }) - .end(done); - }); - }); - - describe('POST [/roles.delete]', () => { - let roleWithUser; - let roleWithoutUser; - before(async () => { - roleWithUser = await createRole(`roleWithUser-${ Date.now() }`, 'Users'); - roleWithoutUser = await createRole(`roleWithoutUser-${ Date.now() }`, 'Users'); - - await addUserToRole(roleWithUser.name, login.user); - }); - - it('should delete a role that it is not being used', (done) => { - request.post(api('roles.delete')) - .set(credentials) - .send({ - roleId: roleWithoutUser._id, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - }) - .end(done); - }); - - it('should NOT delete a role that it is protected', (done) => { - request.post(api('roles.delete')) - .set(credentials) - .send({ - roleId: 'admin', - }) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.nested.property('error', 'Cannot delete a protected role [error-role-protected]'); - }) - .end(done); - }); - - it('should NOT delete a role that it is being used', (done) => { - request.post(api('roles.delete')) - .set(credentials) - .send({ - roleId: roleWithUser._id, - }) - .expect('Content-Type', 'application/json') - .expect(400) - .expect((res) => { - expect(res.body).to.have.property('success', false); - expect(res.body).to.have.nested.property('error', 'Cannot delete role because it\'s in use [error-role-in-use]'); - }) - .end(done); - }); - }); - - describe('POST [/roles.removeUserFromRole]', () => { - let usersScopedRole; - let subscriptionsScopedRole; - - before(async () => { - usersScopedRole = await createRole(`usersScopedRole-${ Date.now() }`, 'Users'); - subscriptionsScopedRole = await createRole(`subscriptionsScopedRole-${ Date.now() }`, 'Subscriptions'); - - await addUserToRole(usersScopedRole.name, login.user); - await addUserToRole(subscriptionsScopedRole.name, login.user, 'GENERAL'); - }); - - it('should unassign a role with User scope from an user', (done) => { - request.post(api('roles.removeUserFromRole')) - .set(credentials) - .send({ - roleName: usersScopedRole.name, - username: login.user, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('role._id', usersScopedRole._id); - expect(res.body).to.have.nested.property('role.name', usersScopedRole.name); - expect(res.body).to.have.nested.property('role.scope', usersScopedRole.scope); - expect(res.body).to.have.nested.property('role.description', usersScopedRole.description); - }) - .end(done); - }); - - it('should unassign a role with Subscriptions scope from an user', (done) => { - request.post(api('roles.removeUserFromRole')) - .set(credentials) - .send({ - roleName: subscriptionsScopedRole.name, - username: login.user, - scope: 'GENERAL', - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.nested.property('role._id', subscriptionsScopedRole._id); - expect(res.body).to.have.nested.property('role.name', subscriptionsScopedRole.name); - expect(res.body).to.have.nested.property('role.scope', subscriptionsScopedRole.scope); - expect(res.body).to.have.nested.property('role.description', subscriptionsScopedRole.description); - }) - .end(done); - }); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index dff6cb6c6dd..1094df397a4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -42,9 +42,10 @@ "exclude": [ "./.meteor/**", "./packages/**", - "./imports/client" + "./imports/client**", + "**/dist/**", // "./ee/server/services/**" - // "node_modules" + "node_modules" ], "ts-node": { "files": true