Chore: Api definitions (#23701)

Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat>
pull/23723/head
Guilherme Gazzo 4 years ago committed by GitHub
parent f05d8932db
commit a858dbc237
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 116
      app/api/server/api.d.ts
  2. 2
      app/api/server/api.js
  3. 2
      app/api/server/helpers/getPaginationItems.js
  4. 23
      app/api/server/v1/banners.ts
  5. 6
      app/api/server/v1/chat.js
  6. 8
      app/api/server/v1/dns.ts
  7. 4
      app/api/server/v1/instances.ts
  8. 12
      app/api/server/v1/ldap.ts
  9. 60
      app/api/server/v1/permissions.ts
  10. 171
      app/api/server/v1/roles.ts
  11. 101
      app/api/server/v1/settings.ts
  12. 233
      app/api/server/v1/teams.ts
  13. 4
      app/authorization/server/functions/getUsersInRole.ts
  14. 2
      app/models/server/raw/BaseRaw.ts
  15. 4
      app/models/server/raw/Permissions.ts
  16. 8
      app/models/server/raw/Roles.ts
  17. 2
      app/models/server/raw/Settings.ts
  18. 2
      client/contexts/ServerContext/ServerContext.ts
  19. 87
      client/contexts/ServerContext/endpoints.ts
  20. 21
      client/contexts/ServerContext/endpoints/v1/dm.ts
  21. 76
      client/contexts/ServerContext/endpoints/v1/teams.ts
  22. 1
      client/contexts/ServerContext/index.ts
  23. 2
      client/hooks/useEndpointAction.ts
  24. 2
      client/hooks/useEndpointActionExperimental.ts
  25. 2
      client/hooks/useEndpointData.ts
  26. 5
      client/providers/ServerProvider.tsx
  27. 3
      client/views/teams/contextualBar/channels/hooks/useTeamsChannelList.ts
  28. 10
      definition/IMessage/IMessage.ts
  29. 1
      definition/IRoom.ts
  30. 2
      definition/IUser.ts
  31. 0
      definition/rest/apps/index.ts
  32. 4
      definition/rest/helpers/PaginatedRequest.ts
  33. 5
      definition/rest/helpers/PaginatedResult.ts
  34. 97
      definition/rest/index.ts
  35. 26
      definition/rest/v1/banners.ts
  36. 6
      definition/rest/v1/channels.ts
  37. 4
      definition/rest/v1/chat.ts
  38. 0
      definition/rest/v1/cloud.ts
  39. 0
      definition/rest/v1/customUserStatus.ts
  40. 21
      definition/rest/v1/dm.ts
  41. 5
      definition/rest/v1/dns.ts
  42. 2
      definition/rest/v1/emojiCustom.ts
  43. 6
      definition/rest/v1/groups.ts
  44. 14
      definition/rest/v1/im.ts
  45. 16
      definition/rest/v1/instances.ts
  46. 6
      definition/rest/v1/ldap.ts
  47. 5
      definition/rest/v1/licenses.ts
  48. 0
      definition/rest/v1/misc.ts
  49. 18
      definition/rest/v1/omnichannel.ts
  50. 43
      definition/rest/v1/permissions.ts
  51. 186
      definition/rest/v1/roles.ts
  52. 6
      definition/rest/v1/rooms.ts
  53. 100
      definition/rest/v1/settings.ts
  54. 2
      definition/rest/v1/statistics.ts
  55. 71
      definition/rest/v1/teams/TeamsAddMembersProps.test.ts
  56. 80
      definition/rest/v1/teams/TeamsAddMembersProps.ts
  57. 39
      definition/rest/v1/teams/TeamsConvertToChannelProps.test.ts
  58. 54
      definition/rest/v1/teams/TeamsConvertToChannelProps.ts
  59. 64
      definition/rest/v1/teams/TeamsDeleteProps.test.ts
  60. 50
      definition/rest/v1/teams/TeamsDeleteProps.ts
  61. 64
      definition/rest/v1/teams/TeamsLeaveProps.test.ts
  62. 51
      definition/rest/v1/teams/TeamsLeaveProps.ts
  63. 61
      definition/rest/v1/teams/TeamsRemoveMemberProps.test.ts
  64. 56
      definition/rest/v1/teams/TeamsRemoveMemberProps.ts
  65. 27
      definition/rest/v1/teams/TeamsRemoveRoomProps.test.ts
  66. 40
      definition/rest/v1/teams/TeamsRemoveRoomProps.ts
  67. 59
      definition/rest/v1/teams/TeamsUpdateMemberProps.test.ts
  68. 68
      definition/rest/v1/teams/TeamsUpdateMemberProps.ts
  69. 161
      definition/rest/v1/teams/TeamsUpdateProps.test.ts
  70. 75
      definition/rest/v1/teams/TeamsUpdateProps.ts
  71. 148
      definition/rest/v1/teams/index.ts
  72. 4
      definition/rest/v1/users.ts
  73. 2
      definition/utils.ts
  74. 11
      ee/app/license/definitions/ILicense.ts
  75. 4
      ee/app/license/definitions/ILicenseTag.ts
  76. 17
      ee/app/license/server/license.ts
  77. 7
      ee/app/livechat-enterprise/server/api/business-hours.ts
  78. 2
      ee/app/livechat-enterprise/server/business-hour/Helper.ts
  79. 10
      ee/client/contexts/ServerContext/endpoints/v1/engagementDashboard.ts
  80. 4
      ee/definition/rest/index.ts
  81. 62
      ee/definition/rest/v1/engagementDashboard.ts
  82. 7
      ee/definition/rest/v1/omnichannel/businessHours.ts
  83. 6
      ee/server/api/ldap.ts
  84. 3
      ee/server/api/licenses.ts
  85. 0
      ee/server/index.ts
  86. 2
      ee/server/lib/ldap/Manager.ts
  87. 39
      ee/server/services/package-lock.json
  88. 1
      ee/server/services/package.json
  89. 80
      package-lock.json
  90. 2
      package.json
  91. 13
      server/sdk/types/ITeamService.ts
  92. 440
      tests/end-to-end/api/13-roles.js
  93. 5
      tsconfig.json

@ -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<string, unknown>;
fields: Record<string, unknown>;
query: Record<string, unknown>;
});
readonly urlParams: Record<string, unknown>;
getUserFromParams(): IUser;
}
type ThisLoggedIn = {
readonly user: IUser;
readonly userId: string;
}
type ThisLoggedOut = {
readonly user: null;
readonly userId: null;
}
type EndpointWithExtraOptions<FN extends (this: any, ...args: any) => any, A> = WrappedFunction<FN> | ({ action: WrappedFunction<FN> } & (A extends true ? { twoFactorRequired: boolean } : {}));
export type Methods<T, A = false> = {
[K in keyof T as `${Lowercase<string & K>}`]: T[K] extends (...args: any) => any ? EndpointWithExtraOptions<(this: This & (A extends true ? ThisLoggedIn : ThisLoggedOut) & Params<K, Parameters<T[K]>[0]>) => ReturnType<T[K]>, A> : never;
};
type Params<K, P> = K extends 'GET' ? { readonly queryParams: Partial<P> } : K extends 'POST' ? { readonly bodyParams: Partial<P> } : never;
type SuccessResult<T = undefined> = {
statusCode: 200;
success: true;
} & T extends (undefined) ? {} : { body: T }
type UnauthorizedResult = {
statusCode: 403;
body: {
success: false;
error: string;
};
}
type FailureResult<T = undefined, ET = undefined, ST = undefined, E = undefined> = {
statusCode: 400;
} & FailureBody<T, ET, ST, { success: false }>;
type FailureBody<T, ET, ST, E> = Exclude<T, string> 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<object> | FailureResult<string, string, string, { error: string }> | UnauthorizedResult;
type WrappedFunction<T extends (this: any, ...args: any) => any> = (this: ThisParameterType<T>, ...args: Parameters<T>) => ReturnTypes<T>;
type ReturnTypes<T extends (this: any, ...args: any) => any> = PromisedOrNot<SuccessResult<ReturnType<T>> | PromisedOrNot<Errors>>;
type PromisedOrNot<T> = Promise<T> | T;
type Options = {
permissionsRequired?: string[];
twoFactorOptions?: unknown;
twoFactorRequired?: boolean;
authRequired?: boolean;
}
export type RestEndpoints<P extends keyof Endpoints, A = false> = Methods<Endpoints[P], A>;
type ToLowerCaseKeys<T> = {
[K in keyof T as `${Lowercase<string & K>}`]: T[K];
};
type ToResultType<T> = {
[K in keyof T]: T[K] extends (...args: any) => any ? Awaited<ReturnTypes<T[K]>> : never;
}
export type ResultTypeEndpoints<P extends keyof Endpoints, A = false> = ToResultType<ToLowerCaseKeys<Endpoints[P]>>;
declare class APIClass {
addRoute<P extends keyof Endpoints>(route: P, endpoints: RestEndpoints<P>): void;
addRoute<P extends keyof Endpoints, O extends Options>(route: P, options: O, endpoints: RestEndpoints<P, O['authRequired'] extends true ? true: false>): void;
unauthorized(msg?: string): UnauthorizedResult;
failure<ET = string | undefined, ST = string | undefined, E = Error | undefined>(result: string, errorType?: ET, stack?: ST, error?: E): FailureResult<string, ET, ST, E>;
failure(result: object): FailureResult<object>;
failure(): FailureResult;
success(): SuccessResult<void>;
success<T>(result: T): SuccessResult<T>;
defaultFieldsToExclude: {
joinCode: 0;
members: 0;
importIds: 0;
e2e: 0;
}
}
export declare const API: {
v1: APIClass;

@ -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,

@ -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 {

@ -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();
},
});

@ -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);
},
});

@ -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 });
},

@ -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) => {

@ -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,
});
},
});

@ -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,

@ -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', {

@ -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<typeof Settings.find>[0], sort: Parameters<typeof Settings.find>[1]['sort'], offset: Parameters<typeof Settings.find>[1]['skip'], count: Parameters<typeof Settings.find>[1]['limit'], fields: Parameters<typeof Settings.find>[1]['projection']): Promise<ISetting[]> => {
@ -24,74 +24,35 @@ const fetchSettings = async (query: Parameters<typeof Settings.find>[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<ResultTypeEndpoints<'settings/:_id', true>['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<ISettingColor>(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();
}

@ -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<IUser>;
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();
},

@ -9,6 +9,6 @@ export function getUsersInRole(name: IRole['name'], scope?: string): Promise<Cur
export function getUsersInRole(name: IRole['name'], scope: string | undefined, options: WithoutProjection<FindOneOptions<IUser>>): Promise<Cursor<IUser>>;
export function getUsersInRole<P>(name: IRole['name'], scope: string | undefined, options: FindOneOptions<P extends IUser ? IUser : P>): Promise<Cursor<P>>;
export function getUsersInRole<P = IUser>(name: IRole['name'], scope: string | undefined, options: FindOneOptions<P extends IUser ? IUser : P>): Promise<Cursor<P extends IUser ? IUser : P>>;
export function getUsersInRole<P>(name: IRole['name'], scope: string | undefined, options?: any | undefined): Promise<Cursor<IUser | P>> { return Roles.findUsersInRole(name, scope, options); }
export function getUsersInRole<P = IUser>(name: IRole['name'], scope: string | undefined, options?: any | undefined): Promise<Cursor<IUser | P>> { return Roles.findUsersInRole(name, scope, options); }

@ -166,7 +166,7 @@ export class BaseRaw<T, C extends DefaultFields<T> = undefined> implements IBase
find(query: FilterQuery<T>, options: WithoutProjection<FindOneOptions<T>>): Cursor<ResultFields<T, C>>;
find<P>(query: FilterQuery<T>, options: FindOneOptions<P extends T ? T : P>): Cursor<P>;
find<P = T>(query: FilterQuery<T>, options: FindOneOptions<P extends T ? T : P>): Cursor<P>;
find<P>(query: FilterQuery<T> | undefined = {}, options?: any): Cursor<P> | Cursor<T> {
const optionsDef = this.doNotMixInclusionAndExclusionFields(options);

@ -30,6 +30,10 @@ export class PermissionsRaw extends BaseRaw<IPermission> {
await this.update({ _id: permission, roles: { $ne: role } }, { $addToSet: { roles: role } });
}
async setRoles(permission: string, roles: string[]): Promise<void> {
await this.update({ _id: permission }, { $set: { roles } });
}
async removeRole(permission: string, role: string): Promise<void> {
await this.update({ _id: permission, roles: role }, { $pull: { roles: role } });
}

@ -17,12 +17,12 @@ export class RolesRaw extends BaseRaw<IRole> {
}
findByUpdatedDate<P>(updatedAfterDate: Date, options: FindOneOptions<P extends IRole ? IRole : P>): Cursor<P> | Cursor<IRole> {
findByUpdatedDate(updatedAfterDate: Date, options?: FindOneOptions<IRole>): Cursor<IRole> {
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<IRole> {
return this.findOne(query, options);
}
updateById(_id: IRole['_id'], name: IRole['name'], scope: IRole['scope'], description: IRole['description'], mandatory2fa: IRole['mandatory2fa']): Promise<UpdateWriteOpResult> {
updateById(_id: IRole['_id'], name: IRole['name'], scope: IRole['scope'], description: IRole['description'] = '', mandatory2fa: IRole['mandatory2fa'] = false): Promise<UpdateWriteOpResult> {
const queryData = {
name,
scope,
@ -151,7 +151,7 @@ export class RolesRaw extends BaseRaw<IRole> {
findUsersInRole(name: IRole['name'], scope: string | undefined, options: WithoutProjection<FindOneOptions<IUser>>): Promise<Cursor<IUser>>;
findUsersInRole<P>(name: IRole['name'], scope: string | undefined, options: FindOneOptions<P extends IUser ? IUser : P>): Promise<Cursor<P>>;
findUsersInRole<P>(name: IRole['name'], scope: string | undefined, options: FindOneOptions<P extends IUser ? IUser : P>): Promise<Cursor<P extends IUser ? IUser : P>>;
async findUsersInRole<P>(name: IRole['name'], scope: string | undefined, options?: any | undefined): Promise<Cursor<IUser> | Cursor<P>> {
const role = await this.findOne({ name }, { scope: 1 } as FindOneOptions<IRole>);

@ -116,7 +116,7 @@ export class SettingsRaw extends BaseRaw<ISetting> {
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<ISetting> {

@ -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,

@ -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<Endpoints>;
type UnionizeEndpoints<EE extends Endpoints> = ValueOf<
{
[P in keyof EE]: UnionizeMethods<P, EE[P]>;
}
>;
type ExtractOperations<OO, M extends keyof OO> = ExtractKeys<OO, M, (...args: any[]) => any>;
type UnionizeMethods<P, OO> = ValueOf<
{
[M in keyof OO as ExtractOperations<OO, M>]: (
method: M,
path: OO extends { path: string } ? OO['path'] : P,
...params: Parameters<Extract<OO[M], (...args: any[]) => any>>
) => ReturnType<Extract<OO[M], (...args: any[]) => any>>;
}
>;
export type Method = Parameters<Endpoint>[0];
export type Path = Parameters<Endpoint>[1];
export type MethodFor<P extends Path> = P extends any
? Parameters<Extract<Endpoint, (method: any, path: P, ...params: any[]) => any>>[0]
: never;
export type PathFor<M extends Method> = M extends any
? Parameters<Extract<Endpoint, (method: M, path: any, ...params: any[]) => any>>[1]
: never;
type Operation<M extends Method, P extends PathFor<M>> = M extends any
? P extends any
? Extract<Endpoint, (method: M, path: P, ...params: any[]) => any>
: never
: never;
type ExtractParams<Q> = Q extends [any, any]
? [undefined?]
: Q extends [any, any, any, ...any[]]
? [Q[2]]
: never;
export type Params<M extends Method, P extends PathFor<M>> = ExtractParams<
Parameters<Operation<M, P>>
>;
export type Return<M extends Method, P extends PathFor<M>> = ReturnType<Operation<M, P>>;

@ -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<IUser['username'], undefined>;
}
| {
usernames: string;
}
) & {
excludeSelf?: boolean;
},
) => {
room: IRoom & { rid: IRoom['_id'] };
};
};
};

@ -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<IRecordsWithTotal<IRoom>, 'records'> & {
count: number;
offset: number;
rooms: IRecordsWithTotal<IRoom>['records'];
};
};
'teams.listRoomsOfUser': {
GET: (params: {
teamId: ITeam['_id'];
teamName?: string;
userId?: string;
canUserDelete?: boolean;
offset?: number;
count?: number;
}) => Omit<IRecordsWithTotal<IRoom>, 'records'> & {
count: number;
offset: number;
rooms: IRecordsWithTotal<IRoom>['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;
};
};
};

@ -1,3 +1,2 @@
export * from './ServerContext';
export * from './endpoints';
export * from './methods';

@ -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 = <M extends Method, P extends PathFor<M>>(

@ -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 = <M extends Method, P extends PathFor<M>>(

@ -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';

@ -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,

@ -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,

@ -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';

@ -63,6 +63,7 @@ export interface IRoom extends IRocketChatRecord {
muted?: string[];
usernames?: string[];
ts?: Date;
}
export interface ICreatedRoom extends IRoom {

@ -95,7 +95,7 @@ export interface IRole {
name: string;
protected: boolean;
// scope?: string;
scope?: 'Users' | 'Subscriptions';
scope: 'Users' | 'Subscriptions';
_id: string;
}

@ -0,0 +1,4 @@
export type PaginatedRequest = {
count: number;
offset: number;
};

@ -0,0 +1,5 @@
export type PaginatedResult = {
count: number;
offset: number;
total: number;
};

@ -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<Endpoints>;
type UnionizeEndpoints<EE extends Endpoints> = ValueOf<
{
[P in keyof EE]: UnionizeMethods<P, EE[P]>;
}
>;
type ExtractOperations<OO, M extends keyof OO> = ExtractKeys<OO, M, (...args: any[]) => any>;
type UnionizeMethods<P, OO> = ValueOf<
{
[M in keyof OO as ExtractOperations<OO, M>]: (
method: M,
path: OO extends { path: string } ? OO['path'] : P,
...params: Parameters<Extract<OO[M], (...args: any[]) => any>>
) => ReturnType<Extract<OO[M], (...args: any[]) => any>>;
}
>;
export type Method = Parameters<Endpoint>[0];
export type Path = Parameters<Endpoint>[1];
export type MethodFor<P extends Path> = P extends any
? Parameters<Extract<Endpoint, (method: any, path: P, ...params: any[]) => any>>[0]
: never;
export type PathFor<M extends Method> = M extends any
? Parameters<Extract<Endpoint, (method: M, path: any, ...params: any[]) => any>>[1]
: never;
type Operation<M extends Method, P extends PathFor<M>> = M extends any
? P extends any
? Extract<Endpoint, (method: M, path: P, ...params: any[]) => any>
: never
: never;
type ExtractParams<Q> = Q extends [any, any]
? [undefined?]
: Q extends [any, any, any, ...any[]]
? [Q[2]]
: never;
export type Params<M extends Method, P extends PathFor<M>> = ExtractParams<
Parameters<Operation<M, P>>
>;
export type Return<M extends Method, P extends PathFor<M>> = ReturnType<Operation<M, P>>;

@ -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;
};
};

@ -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': {

@ -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': {

@ -0,0 +1,21 @@
import type { IRoom } from '../../IRoom';
import type { IUser } from '../../IUser';
export type DmEndpoints = {
'dm.create': {
POST: (
params: (
| {
username: Exclude<IUser['username'], undefined>;
}
| {
usernames: string;
}
) & {
excludeSelf?: boolean;
},
) => {
room: IRoom & { rid: IRoom['_id'] };
};
};
};

@ -5,8 +5,9 @@ export type DnsEndpoints = {
};
};
'dns.resolve.txt': {
GET: (params: { url: string }) => {
resolved: Record<string, string | number>;
POST: (params: { url: string }) => {
resolved: string;
// resolved: Record<string, string | number>;
};
};
};

@ -1,4 +1,4 @@
import type { ICustomEmojiDescriptor } from '../../../../../definition/ICustomEmojiDescriptor';
import type { ICustomEmojiDescriptor } from '../../ICustomEmojiDescriptor';
export type EmojiCustomEndpoints = {
'emoji-custom.list': {

@ -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': {

@ -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<IUser['username'], undefined>;
}
username: Exclude<IUser['username'], undefined>;
}
| {
usernames: string;
}
usernames: string;
}
) & {
excludeSelf?: boolean;
},

@ -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;
};
})[];
});
};
};

@ -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: () => {

@ -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<ILicense> };
};
'licenses.add': {
POST: (params: { license: string }) => void;
};
'licenses.maxActiveUsers': {
GET: () => { maxActiveUsers: number | null; activeUsers: number };
};

@ -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;
};

@ -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<PermissionsUpdateProps> = {
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[];
});
};
};

@ -0,0 +1,186 @@
import Ajv, { JSONSchemaType } from 'ajv';
import { IRole, IUser } from '../../IUser';
const ajv = new Ajv();
type RoleCreateProps = Pick<IRole, 'name'> & Partial<Pick<IRole, 'description' | 'scope' |'mandatory2fa' >>;
const roleCreatePropsSchema: JSONSchemaType<RoleCreateProps> = {
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<RoleCreateProps>;
const roleUpdatePropsSchema: JSONSchemaType<RoleUpdateProps> = {
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<RoleDeleteProps> = {
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<RoleAddUserToRoleProps> = {
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<RoleRemoveUserFromRoleProps> = {
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;
});
};
};

@ -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': {

@ -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<SettingsUpdateProps>): props is SettingsUpdatePropsActions => 'execute' in props;
type SettingsUpdatePropsColor = {
editor: ISettingColor['editor'];
value: ISetting['value'];
}
export const isSettingsUpdatePropsColor = (props: Partial<SettingsUpdateProps>): props is SettingsUpdatePropsColor => 'editor' in props && 'value' in props;
type SettingsUpdatePropDefault = {
value: ISetting['value'];
}
export const isSettingsUpdatePropDefault = (props: Partial<SettingsUpdateProps>): props is SettingsUpdatePropDefault => 'value' in props;
export type SettingsEndpoints = {
'settings.public': {
GET: () => PaginatedResult & {
settings: Array<ISetting>;
};
};
'settings.oauth': {
GET: () => ({
services: Partial<OauthCustomConfiguration>[];
});
};
'settings.addCustomOAuth': {
POST: (params: { name: string }) => void;
};
'settings': {
GET: () => ({
settings: ISetting[];
});
};
'settings/:_id': {
GET: () => Pick<ISetting, '_id' | 'value'>;
POST: (params: SettingsUpdateProps) => void;
};
'service.configurations': {
GET: () => {
configurations: Array<{
appId: string;
secret: string;
}>;
};
};
};

@ -1,4 +1,4 @@
import type { IStats } from '../../../../../definition/IStats';
import type { IStats } from '../../IStats';
export type StatisticsEndpoints = {
statistics: {

@ -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 }));
});
});
});

@ -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<TeamsAddMembersProps> = {
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);

@ -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' }));
});
});
});

@ -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<TeamsConvertToChannelProps> = {
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);

@ -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' }));
});
});
});

@ -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<TeamsDeleteProps> = {
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);

@ -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' }));
});
});
});

@ -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<TeamsLeaveProps> = {
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);

@ -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' }));
});
});
});

@ -0,0 +1,56 @@
import Ajv, { JSONSchemaType } from 'ajv';
const ajv = new Ajv();
export type TeamsRemoveMemberProps = ({ teamId: string } | { teamName: string }) & { userId: string; rooms?: Array<string> };
const teamsRemoveMemberPropsSchema: JSONSchemaType<TeamsRemoveMemberProps> = {
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);

@ -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' }));
});
});
});

@ -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<TeamsRemoveRoomProps> = {
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);

@ -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 }));
});
});
});

@ -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<TeamsUpdateMemberProps> = {
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);

@ -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,
},
}));
});
});
});

@ -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<TeamsUpdateProps> = {
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);

@ -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 = <T extends TeamProps>(props: T): props is T & { teamName: string } => 'teamName' in props;
export const isTeamPropsWithTeamId = <T extends TeamProps>(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<ITeam> });
};
'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 });
};
};

@ -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': {

@ -7,3 +7,5 @@ export type ValueOf<T> = T[keyof T];
export type UnionToIntersection<T> = (T extends any ? (x: T) => void : never) extends (x: infer U) => void
? U
: never;
export type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;

@ -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;
}

@ -0,0 +1,4 @@
export interface ILicenseTag {
name: string;
color: string;
}

@ -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;

@ -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));
},
});

@ -10,7 +10,7 @@ import {
import { ILivechatBusinessHour, LivechatBusinessHourTypes } from '../../../../../definition/ILivechatBusinessHour';
const getAllAgentIdsWithoutDepartment = async (): Promise<string[]> => {
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);

@ -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<IDailyActiveUsers>[];
};
};
};

@ -0,0 +1,4 @@
import type { EngagementDashboardEndpoints } from './v1/engagementDashboard';
import type { OmnichannelBusinessHoursEndpoints } from './v1/omnichannel/businessHours';
export type EnterpriseEndpoints = EngagementDashboardEndpoints & OmnichannelBusinessHoursEndpoints;

@ -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<IDailyActiveUsers>[];
};
};
};

@ -0,0 +1,7 @@
import { ILivechatBusinessHour } from '../../../../../definition/ILivechatBusinessHour';
export type OmnichannelBusinessHoursEndpoints = {
'livechat/business-hours.list': {
GET: () => ({ businessHours: ILivechatBusinessHour[] });
};
}

@ -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,
});
},
});

@ -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 {

@ -192,7 +192,7 @@ export class LDAPEEManager extends LDAPManager {
}
const roles = await Roles.find({}, {
fields: {
projection: {
_updatedAt: 0,
},
}).toArray() as Array<IRole>;

@ -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",

@ -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",

80
package-lock.json generated

@ -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": {

@ -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",

@ -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<IRoom, '_id' | 'fname' | 'teamId' | 'name' | 't' | 'avatarETag'>;

@ -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);
});
});
});

@ -42,9 +42,10 @@
"exclude": [
"./.meteor/**",
"./packages/**",
"./imports/client"
"./imports/client**",
"**/dist/**",
// "./ee/server/services/**"
// "node_modules"
"node_modules"
],
"ts-node": {
"files": true

Loading…
Cancel
Save