[IMPROVE] Stricter API types (#23735)

Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>
pull/23762/head
Tasso Evangelista 5 years ago committed by GitHub
parent 9a3c2cf00b
commit c83c0be485
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 203
      app/api/server/api.d.ts
  2. 39
      app/api/server/v1/banners.ts
  3. 45
      app/api/server/v1/roles.ts
  4. 4
      app/api/server/v1/settings.ts
  5. 147
      app/api/server/v1/teams.ts
  6. 56
      app/models/server/raw/Sessions.ts
  7. 43
      app/utils/client/lib/RestApiClient.d.ts
  8. 1
      app/utils/client/lib/RestApiClient.js
  9. 35
      client/contexts/ServerContext/ServerContext.ts
  10. 20
      client/hooks/useEndpointAction.ts
  11. 21
      client/hooks/useEndpointActionExperimental.ts
  12. 24
      client/hooks/useEndpointData.ts
  13. 34
      client/lib/userData.ts
  14. 18
      client/providers/ServerProvider.tsx
  15. 17
      client/startup/banners.ts
  16. 8
      client/views/admin/settings/groups/LDAPGroupPage.tsx
  17. 4
      client/views/hooks/useDepartmentsByUnitsList.ts
  18. 19
      definition/Serialized.ts
  19. 7
      definition/externals/meteor/check.d.ts
  20. 8
      definition/rest/helpers/ReplacePlaceholders.ts
  21. 111
      definition/rest/index.ts
  22. 8
      definition/rest/v1/banners.ts
  23. 11
      definition/rest/v1/omnichannel.ts
  24. 5
      definition/rest/v1/roles.ts
  25. 28
      definition/rest/v1/teams/index.ts
  26. 3
      definition/utils.ts
  27. 12
      ee/definition/rest/v1/omnichannel/businessHours.ts
  28. 2
      server/sdk/types/ITeamService.ts
  29. 7
      tests/end-to-end/api/21-banners.js

@ -1,77 +1,44 @@
import { Endpoints } from '../../../definition/rest';
import { Awaited } from '../../../definition/utils';
import { IUser } from '../../../definition/IUser';
import type { JoinPathPattern, Method, MethodOf, OperationParams, OperationResult, PathPattern, UrlParams } from '../../../definition/rest';
import type { 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 SuccessResult<T> = {
statusCode: 200;
body:
T extends object
? { success: true } & T
: T;
};
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 FailureResult<T, TStack = undefined, TErrorType = undefined, TErrorDetails = undefined> = {
statusCode: 400;
body:
T extends object
? { success: false } & T
: ({
success: false;
error: T;
stack: TStack;
errorType: TErrorType;
details: TErrorDetails;
}) & (
undefined extends TErrorType
? {}
: { errorType: TErrorType }
) & (
undefined extends TErrorDetails
? {}
: { details: TErrorDetails extends string ? unknown : TErrorDetails }
);
};
type UnauthorizedResult = {
type UnauthorizedResult<T> = {
statusCode: 403;
body: {
success: false;
error: string;
error: T | 'unauthorized';
};
}
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;
@ -79,32 +46,102 @@ type Options = {
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 ActionThis<TMethod extends Method, TPathPattern extends PathPattern, TOptions> = {
urlParams: UrlParams<TPathPattern>;
// TODO make it unsafe
readonly queryParams: TMethod extends 'GET' ? Partial<OperationParams<TMethod, TPathPattern>> : Record<string, string>;
// TODO make it unsafe
readonly bodyParams: TMethod extends 'GET' ? Record<string, unknown> : Partial<OperationParams<TMethod, TPathPattern>>;
requestParams(): OperationParams<TMethod, TPathPattern>;
getPaginationItems(): {
readonly offset: number;
readonly count: number;
};
parseJsonQuery(): {
sort: Record<string, unknown>;
fields: Record<string, unknown>;
query: Record<string, unknown>;
};
getUserFromParams(): IUser;
} & (
TOptions extends { authRequired: true }
? {
readonly user: IUser;
readonly userId: string;
}
: {
readonly user: null;
readonly userId: null;
}
);
export type ResultFor<
TMethod extends Method,
TPathPattern extends PathPattern
> = SuccessResult<OperationResult<TMethod, TPathPattern>> | FailureResult<unknown, unknown, unknown, unknown> | UnauthorizedResult<unknown>;
type Action<TMethod extends Method, TPathPattern extends PathPattern, TOptions> =
((this: ActionThis<TMethod, TPathPattern, TOptions>) => Promise<ResultFor<TMethod, TPathPattern>>)
| ((this: ActionThis<TMethod, TPathPattern, TOptions>) => ResultFor<TMethod, TPathPattern>);
type Operation<TMethod extends Method, TPathPattern extends PathPattern, TEndpointOptions> = Action<TMethod, TPathPattern, TEndpointOptions> | {
action: Action<TMethod, TPathPattern, TEndpointOptions>;
} & ({ twoFactorRequired: boolean });
type Operations<TPathPattern extends PathPattern, TOptions extends Options = {}> = {
[M in MethodOf<TPathPattern> as Lowercase<M>]: Operation<Uppercase<M>, TPathPattern, TOptions>;
};
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;
declare class APIClass<TBasePath extends string = '/'> {
addRoute<
TSubPathPattern extends string
>(subpath: TSubPathPattern, operations: Operations<JoinPathPattern<TBasePath, TSubPathPattern>>): void;
addRoute<
TSubPathPattern extends string,
TPathPattern extends JoinPathPattern<TBasePath, TSubPathPattern>
>(subpaths: TSubPathPattern[], operations: Operations<TPathPattern>): void;
addRoute<
TSubPathPattern extends string,
TOptions extends Options
>(
subpath: TSubPathPattern,
options: TOptions,
operations: Operations<JoinPathPattern<TBasePath, TSubPathPattern>, TOptions>
): void;
addRoute<
TSubPathPattern extends string,
TPathPattern extends JoinPathPattern<TBasePath, TSubPathPattern>,
TOptions extends Options
>(
subpaths: TSubPathPattern[],
options: TOptions,
operations: Operations<TPathPattern, TOptions>
): void;
addRoute<P extends keyof Endpoints, O extends Options>(route: P, options: O, endpoints: RestEndpoints<P, O['authRequired'] extends true ? true: false>): void;
success<T>(result: T): SuccessResult<T>;
unauthorized(msg?: string): UnauthorizedResult;
success(): SuccessResult<void>;
failure<ET = string | undefined, ST = string | undefined, E = Error | undefined>(result: string, errorType?: ET, stack?: ST, error?: E): FailureResult<string, ET, ST, E>;
failure<
T,
TErrorType extends string,
TStack extends string,
TErrorDetails
>(
result: T,
errorType?: TErrorType,
stack?: TStack,
error?: { details: TErrorDetails }
): FailureResult<T, TErrorType, TStack, TErrorDetails>;
failure(result: object): FailureResult<object>;
failure<T>(result: T): FailureResult<T>;
failure(): FailureResult;
failure(): FailureResult<void>;
success(): SuccessResult<void>;
success<T>(result: T): SuccessResult<T>;
unauthorized<T>(msg?: T): UnauthorizedResult<T>;
defaultFieldsToExclude: {
joinCode: 0;
@ -115,6 +152,6 @@ declare class APIClass {
}
export declare const API: {
v1: APIClass;
v1: APIClass<'/v1'>;
default: APIClass;
};

@ -1,4 +1,3 @@
import { Meteor } from 'meteor/meteor';
import { Match, check } from 'meteor/check';
import { API } from '../api';
@ -54,20 +53,13 @@ import { BannerPlatform } from '../../../../definition/IBanner';
API.v1.addRoute('banners.getNew', { authRequired: true }, { // deprecated
async get() {
check(this.queryParams, Match.ObjectIncluding({
platform: String,
platform: Match.OneOf(...Object.values(BannerPlatform)),
bid: Match.Maybe(String),
}));
const { platform, bid: bannerId } = this.queryParams;
if (!platform) {
throw new Meteor.Error('error-missing-param', 'The required "platform" param is missing.');
}
if (!Object.values(BannerPlatform).includes(platform)) {
throw new Meteor.Error('error-unknown-platform', 'Platform is unknown.');
}
const banners = await Banner.getBannersForUser(this.userId, platform, bannerId);
const banners = await Banner.getBannersForUser(this.userId, platform, bannerId ?? undefined);
return API.v1.success({ banners });
},
@ -122,23 +114,15 @@ API.v1.addRoute('banners.getNew', { authRequired: true }, { // deprecated
API.v1.addRoute('banners/:id', { authRequired: true }, { // TODO: move to users/:id/banners
async get() {
check(this.urlParams, Match.ObjectIncluding({
id: String,
id: Match.Where((id: unknown): id is string => typeof id === 'string' && Boolean(id.trim())),
}));
check(this.queryParams, Match.ObjectIncluding({
platform: String,
platform: Match.OneOf(...Object.values(BannerPlatform)),
}));
const { platform } = this.queryParams;
if (!platform) {
throw new Meteor.Error('error-missing-param', 'The required "platform" param is missing.');
}
const { id } = this.urlParams;
if (!id) {
throw new Meteor.Error('error-missing-param', 'The required "id" param is missing.');
}
const banners = await Banner.getBannersForUser(this.userId, platform, id);
@ -186,17 +170,10 @@ API.v1.addRoute('banners/:id', { authRequired: true }, { // TODO: move to users/
API.v1.addRoute('banners', { authRequired: true }, {
async get() {
check(this.queryParams, Match.ObjectIncluding({
platform: String,
platform: Match.OneOf(...Object.values(BannerPlatform)),
}));
const { platform } = this.queryParams;
if (!platform) {
throw new Meteor.Error('error-missing-param', 'The required "platform" param is missing.');
}
if (!Object.values(BannerPlatform).includes(platform)) {
throw new Meteor.Error('error-unknown-platform', 'Platform is unknown.');
}
const banners = await Banner.getBannersForUser(this.userId, platform);
@ -240,15 +217,11 @@ API.v1.addRoute('banners', { authRequired: true }, {
API.v1.addRoute('banners.dismiss', { authRequired: true }, {
async post() {
check(this.bodyParams, Match.ObjectIncluding({
bannerId: String,
bannerId: Match.Where((id: unknown): id is string => typeof id === 'string' && Boolean(id.trim())),
}));
const { bannerId } = this.bodyParams;
if (!bannerId || !bannerId.trim()) {
throw new Meteor.Error('error-missing-param', 'The required "bannerId" param is missing.');
}
await Banner.dismiss(this.userId, bannerId);
return API.v1.success();
},

@ -1,4 +1,5 @@
import { Meteor } from 'meteor/meteor';
import { check, Match } from 'meteor/check';
import { Users } from '../../../models/server';
import { API } from '../api';
@ -20,11 +21,11 @@ API.v1.addRoute('roles.list', { authRequired: true }, {
API.v1.addRoute('roles.sync', { authRequired: true }, {
async get() {
const { updatedSince } = this.queryParams;
check(this.queryParams, Match.ObjectIncluding({
updatedSince: Match.Where((value: unknown): value is string => typeof value === 'string' && !Number.isNaN(Date.parse(value))),
}));
if (isNaN(Date.parse(updatedSince))) {
throw new Meteor.Error('error-updatedSince-param-invalid', 'The "updatedSince" query parameter must be a valid date.');
}
const { updatedSince } = this.queryParams;
return API.v1.success({
roles: {
@ -41,25 +42,23 @@ API.v1.addRoute('roles.create', { authRequired: true }, {
throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.');
}
const roleData = {
name: this.bodyParams.name,
scope: this.bodyParams.scope,
description: this.bodyParams.description,
mandatory2fa: this.bodyParams.mandatory2fa,
};
const { name, scope, description, mandatory2fa } = this.bodyParams;
if (!await hasPermissionAsync(Meteor.userId(), 'access-permissions')) {
throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed');
}
if (await Roles.findOneByIdOrName(roleData.name)) {
if (await Roles.findOneByIdOrName(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 roleId = (await Roles.createWithRandomId(roleData.name, roleData.scope, roleData.description, false, roleData.mandatory2fa)).insertedId;
const roleId = (await Roles.createWithRandomId(
name,
scope && ['Users', 'Subscriptions'].includes(scope) ? scope : 'Users',
description,
false,
mandatory2fa,
)).insertedId;
if (settings.get('UI_DisplayRoles')) {
api.broadcast('user.roleUpdate', {
@ -236,29 +235,25 @@ API.v1.addRoute('roles.removeUserFromRole', { authRequired: true }, {
throw new Meteor.Error('error-invalid-role-properties', 'The role properties are invalid.');
}
const data = {
roleName: bodyParams.roleName,
username: bodyParams.username,
scope: this.bodyParams.scope,
};
const { roleName, username, scope } = bodyParams;
if (!await hasPermissionAsync(this.userId, 'access-permissions')) {
throw new Meteor.Error('error-not-allowed', 'Accessing permissions is not allowed');
}
const user = Users.findOneByUsername(data.username);
const user = Users.findOneByUsername(username);
if (!user) {
throw new Meteor.Error('error-invalid-user', 'There is no user with this username');
}
const role = await Roles.findOneByIdOrName(data.roleName);
const role = await Roles.findOneByIdOrName(roleName);
if (!role) {
throw new Meteor.Error('error-invalid-roleId', 'This role does not exist');
}
if (!await hasRoleAsync(user._id, role.name, data.scope)) {
if (!await hasRoleAsync(user._id, role.name, scope)) {
throw new Meteor.Error('error-user-not-in-role', 'User is not in this role');
}
@ -269,7 +264,7 @@ API.v1.addRoute('roles.removeUserFromRole', { authRequired: true }, {
}
}
await Roles.removeUserRoles(user._id, [role.name], data.scope);
await Roles.removeUserRoles(user._id, [role.name], scope);
if (settings.get('UI_DisplayRoles')) {
api.broadcast('user.roleUpdate', {
@ -279,7 +274,7 @@ API.v1.addRoute('roles.removeUserFromRole', { authRequired: true }, {
_id: user._id,
username: user.username,
},
scope: data.scope,
scope,
});
}

@ -4,7 +4,7 @@ import _ from 'underscore';
import { Settings } from '../../../models/server/raw';
import { hasPermission } from '../../../authorization/server';
import { API, ResultTypeEndpoints } from '../api';
import { API, ResultFor } from '../api';
import { SettingsEvents, settings } from '../../../settings/server';
import { setValue } from '../../../settings/server/raw';
import { ISetting, ISettingColor, isSettingAction, isSettingColor } from '../../../../definition/ISetting';
@ -126,7 +126,7 @@ API.v1.addRoute('settings/:_id', { authRequired: true }, {
},
post: {
twoFactorRequired: true,
async action(): Promise<ResultTypeEndpoints<'settings/:_id', true>['post']> {
async action(): Promise<ResultFor<'POST', 'settings/:_id'>> {
if (!hasPermission(this.userId, 'edit-privileged-setting')) {
return API.v1.unauthorized();
}

@ -18,6 +18,7 @@ import { isTeamsAddMembersProps } from '../../../../definition/rest/v1/teams/Tea
import { isTeamsDeleteProps } from '../../../../definition/rest/v1/teams/TeamsDeleteProps';
import { isTeamsLeaveProps } from '../../../../definition/rest/v1/teams/TeamsLeaveProps';
import { isTeamsUpdateProps } from '../../../../definition/rest/v1/teams/TeamsUpdateProps';
import { ITeam, TEAM_TYPE } from '../../../../definition/ITeam';
API.v1.addRoute('teams.list', { authRequired: true }, {
async get() {
@ -59,11 +60,16 @@ API.v1.addRoute('teams.create', { authRequired: true }, {
if (!hasPermission(this.userId, 'create-team')) {
return API.v1.unauthorized();
}
const { name, type, members, room, owner } = this.bodyParams;
if (!name) {
return API.v1.failure('Body param "name" is required');
}
check(this.bodyParams, Match.ObjectIncluding({
name: String,
type: Match.OneOf(TEAM_TYPE.PRIVATE, TEAM_TYPE.PUBLIC),
members: Match.Maybe([String]),
room: Match.Maybe(Match.Any),
owner: Match.Maybe(String),
}));
const { name, type, members, room, owner } = this.bodyParams;
const team = await Team.create(this.userId, {
team: {
@ -120,15 +126,34 @@ API.v1.addRoute('teams.convertToChannel', { authRequired: true }, {
},
});
const getTeamByIdOrName = async (params: { teamId: string } | { teamName: string }): Promise<ITeam | null> => {
if ('teamId' in params && params.teamId) {
return Team.getOneById<ITeam>(params.teamId);
}
if ('teamName' in params && params.teamName) {
return Team.getOneByName(params.teamName);
}
return null;
};
API.v1.addRoute('teams.addRooms', { authRequired: true }, {
async post() {
const { rooms, teamId, teamName } = this.bodyParams;
if (!teamId && !teamName) {
return API.v1.failure('missing-teamId-or-teamName');
}
check(this.bodyParams, Match.OneOf(
Match.ObjectIncluding({
teamId: String,
}),
Match.ObjectIncluding({
teamName: String,
}),
));
check(this.bodyParams, Match.ObjectIncluding({
rooms: [String],
}));
const team = await (teamId ? Team.getOneById(teamId) : Team.getOneByName(teamName));
const team = await getTeamByIdOrName(this.bodyParams);
if (!team) {
return API.v1.failure('team-does-not-exist');
}
@ -137,6 +162,8 @@ API.v1.addRoute('teams.addRooms', { authRequired: true }, {
return API.v1.unauthorized('error-no-permission-team-channel');
}
const { rooms } = this.bodyParams;
const validRooms = await Team.addRooms(this.userId, rooms, team._id);
return API.v1.success({ rooms: validRooms });
@ -148,9 +175,8 @@ API.v1.addRoute('teams.removeRoom', { authRequired: true }, {
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 = await (teamId ? Team.getOneById(teamId) : Team.getOneByName(teamName));
const team = await getTeamByIdOrName(this.bodyParams);
if (!team) {
return API.v1.failure('team-does-not-exist');
}
@ -161,6 +187,8 @@ API.v1.addRoute('teams.removeRoom', { authRequired: true }, {
const canRemoveAny = !!hasPermission(this.userId, 'view-all-team-channels', team.roomId);
const { roomId } = this.bodyParams;
const room = await Team.removeRoom(this.userId, roomId, team._id, canRemoveAny);
return API.v1.success({ room });
@ -169,6 +197,11 @@ API.v1.addRoute('teams.removeRoom', { authRequired: true }, {
API.v1.addRoute('teams.updateRoom', { authRequired: true }, {
async post() {
check(this.bodyParams, Match.ObjectIncluding({
roomId: String,
isDefault: Boolean,
}));
const { roomId, isDefault } = this.bodyParams;
const team = await Team.getOneByRoomId(roomId);
@ -189,15 +222,29 @@ API.v1.addRoute('teams.updateRoom', { authRequired: true }, {
API.v1.addRoute('teams.listRooms', { authRequired: true }, {
async get() {
const { teamId, teamName, filter, type } = this.queryParams;
check(this.queryParams, Match.OneOf(
Match.ObjectIncluding({
teamId: String,
}),
Match.ObjectIncluding({
teamName: String,
}),
));
check(this.queryParams, Match.ObjectIncluding({
filter: Match.Maybe(String),
type: Match.Maybe(String),
}));
const { filter, type } = this.queryParams;
const { offset, count } = this.getPaginationItems();
const team = await (teamId ? Team.getOneById(teamId) : Team.getOneByName(teamName));
const team = await getTeamByIdOrName(this.queryParams);
if (!team) {
return API.v1.failure('team-does-not-exist');
}
const allowPrivateTeam = hasPermission(this.userId, 'view-all-teams', team.roomId);
const allowPrivateTeam: boolean = hasPermission(this.userId, 'view-all-teams', team.roomId);
let getAllRooms = false;
if (hasPermission(this.userId, 'view-all-team-channels', team.roomId)) {
@ -205,7 +252,7 @@ API.v1.addRoute('teams.listRooms', { authRequired: true }, {
}
const listFilter = {
name: filter,
name: filter ?? undefined,
isDefault: type === 'autoJoin',
getAllRooms,
allowPrivateTeam,
@ -224,27 +271,36 @@ API.v1.addRoute('teams.listRooms', { authRequired: true }, {
API.v1.addRoute('teams.listRoomsOfUser', { authRequired: true }, {
async get() {
const { offset, count } = this.getPaginationItems();
const { teamId, teamName, userId, canUserDelete = false } = this.queryParams;
check(this.queryParams, Match.OneOf(
Match.ObjectIncluding({
teamId: String,
}),
Match.ObjectIncluding({
teamName: String,
}),
));
if (!teamId && !teamName) {
return API.v1.failure('missing-teamId-or-teamName');
}
check(this.queryParams, Match.ObjectIncluding({
userId: String,
canUserDelete: Match.Maybe(Boolean),
}));
const team = await (teamId ? Team.getOneById(teamId) : Team.getOneByName(teamName!));
const { offset, count } = this.getPaginationItems();
const team = await getTeamByIdOrName(this.queryParams);
if (!team) {
return API.v1.failure('team-does-not-exist');
}
const allowPrivateTeam = hasPermission(this.userId, 'view-all-teams', team.roomId);
const { userId, canUserDelete } = this.queryParams;
if (!(this.userId === userId || hasPermission(this.userId, 'view-all-team-channels', team.roomId))) {
return API.v1.unauthorized();
}
const { records, total } = 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 ?? false, { offset, count });
return API.v1.success({
rooms: records,
@ -259,23 +315,28 @@ API.v1.addRoute('teams.members', { authRequired: true }, {
async get() {
const { offset, count } = this.getPaginationItems();
check(this.queryParams, Match.OneOf(
Match.ObjectIncluding({
teamId: String,
}),
Match.ObjectIncluding({
teamName: String,
}),
));
check(this.queryParams, Match.ObjectIncluding({
teamId: Match.Maybe(String),
teamName: Match.Maybe(String),
status: Match.Maybe([String]),
username: Match.Maybe(String),
name: Match.Maybe(String),
}));
const { teamId, teamName, status, username, name } = this.queryParams;
if (!teamId && !teamName) {
return API.v1.failure('missing-teamId-or-teamName');
}
const { status, username, name } = this.queryParams;
const team = await (teamId ? Team.getOneById(teamId) : Team.getOneByName(teamName));
const team = await getTeamByIdOrName(this.queryParams);
if (!team) {
return API.v1.failure('team-does-not-exist');
}
const canSeeAllMembers = hasPermission(this.userId, 'view-all-teams', team.roomId);
const query = {
@ -431,16 +492,16 @@ API.v1.addRoute('teams.leave', { authRequired: true }, {
API.v1.addRoute('teams.info', { authRequired: true }, {
async get() {
const { teamId, teamName } = this.queryParams;
if (!teamId && !teamName) {
return API.v1.failure('Provide either the "teamId" or "teamName"');
}
const teamInfo = await (teamId
? Team.getInfoById(teamId)
: Team.getInfoByName(teamName));
check(this.queryParams, Match.OneOf(
Match.ObjectIncluding({
teamId: String,
}),
Match.ObjectIncluding({
teamName: String,
}),
));
const teamInfo = await getTeamByIdOrName(this.queryParams);
if (!teamInfo) {
return API.v1.failure('Team not found');
}
@ -498,6 +559,10 @@ API.v1.addRoute('teams.delete', { authRequired: true }, {
API.v1.addRoute('teams.autocomplete', { authRequired: true }, {
async get() {
check(this.queryParams, Match.ObjectIncluding({
name: String,
}));
const { name } = this.queryParams;
const teams = await Team.autocomplete(this.userId, name);

@ -1,14 +1,14 @@
import { AggregationCursor, BulkWriteOperation, BulkWriteOpResultObject, Collection, IndexSpecification, UpdateWriteOpResult, FilterQuery } from 'mongodb';
import { ISession as T } from '../../../../definition/ISession';
import { ISession } from '../../../../definition/ISession';
import { BaseRaw, ModelOptionalId } from './BaseRaw';
type DestructuredDate = {year: number; month: number; day: number};
type DestructuredDateWithType = {year: number; month: number; day: number; type?: 'month' | 'week'};
type DestructuredRange = {start: DestructuredDate; end: DestructuredDate};
type FullReturn = { year: number; month: number; day: number; data: T[] };
type FullReturn = { year: number; month: number; day: number; data: ISession[] };
const matchBasedOnDate = (start: DestructuredDate, end: DestructuredDate): FilterQuery<T> => {
const matchBasedOnDate = (start: DestructuredDate, end: DestructuredDate): FilterQuery<ISession> => {
if (start.year === end.year && start.month === end.month) {
return {
year: start.year,
@ -112,16 +112,16 @@ const getProjectionByFullDate = (): { day: string; month: string; year: string }
});
export const aggregates = {
dailySessionsOfYesterday(collection: Collection<T>, { year, month, day }: DestructuredDate): AggregationCursor<Pick<T, 'mostImportantRole' | 'userId' | 'day' | 'year' | 'month' | 'type'> & {
dailySessionsOfYesterday(collection: Collection<ISession>, { year, month, day }: DestructuredDate): AggregationCursor<Pick<ISession, 'mostImportantRole' | 'userId' | 'day' | 'year' | 'month' | 'type'> & {
time: number;
sessions: number;
devices: T['device'][];
devices: ISession['device'][];
_computedAt: string;
}> {
return collection.aggregate<Pick<T, 'mostImportantRole' | 'userId' | 'day' | 'year' | 'month' | 'type'> & {
return collection.aggregate<Pick<ISession, 'mostImportantRole' | 'userId' | 'day' | 'year' | 'month' | 'type'> & {
time: number;
sessions: number;
devices: T['device'][];
devices: ISession['device'][];
_computedAt: string;
}>([{
$match: {
@ -211,7 +211,7 @@ export const aggregates = {
}], { allowDiskUse: true });
},
async getUniqueUsersOfYesterday(collection: Collection<T>, { year, month, day }: DestructuredDate): Promise<T[]> {
async getUniqueUsersOfYesterday(collection: Collection<ISession>, { year, month, day }: DestructuredDate): Promise<ISession[]> {
return collection.aggregate([{
$match: {
year,
@ -273,7 +273,7 @@ export const aggregates = {
}]).toArray();
},
async getUniqueUsersOfLastMonthOrWeek(collection: Collection<T>, { year, month, day, type = 'month' }: DestructuredDateWithType): Promise<T[]> {
async getUniqueUsersOfLastMonthOrWeek(collection: Collection<ISession>, { year, month, day, type = 'month' }: DestructuredDateWithType): Promise<ISession[]> {
return collection.aggregate([{
$match: {
type: 'user_daily',
@ -343,7 +343,7 @@ export const aggregates = {
}], { allowDiskUse: true }).toArray();
},
getMatchOfLastMonthOrWeek({ year, month, day, type = 'month' }: DestructuredDateWithType): FilterQuery<T> {
getMatchOfLastMonthOrWeek({ year, month, day, type = 'month' }: DestructuredDateWithType): FilterQuery<ISession> {
let startOfPeriod;
if (type === 'month') {
@ -418,7 +418,7 @@ export const aggregates = {
};
},
async getUniqueDevicesOfLastMonthOrWeek(collection: Collection<T>, { year, month, day, type = 'month' }: DestructuredDateWithType): Promise<T[]> {
async getUniqueDevicesOfLastMonthOrWeek(collection: Collection<ISession>, { year, month, day, type = 'month' }: DestructuredDateWithType): Promise<ISession[]> {
return collection.aggregate([{
$match: {
type: 'user_daily',
@ -456,7 +456,7 @@ export const aggregates = {
}], { allowDiskUse: true }).toArray();
},
getUniqueDevicesOfYesterday(collection: Collection<T>, { year, month, day }: DestructuredDate): Promise<T[]> {
getUniqueDevicesOfYesterday(collection: Collection<ISession>, { year, month, day }: DestructuredDate): Promise<ISession[]> {
return collection.aggregate([{
$match: {
year,
@ -496,7 +496,7 @@ export const aggregates = {
}]).toArray();
},
getUniqueOSOfLastMonthOrWeek(collection: Collection<T>, { year, month, day, type = 'month' }: DestructuredDateWithType): Promise<T[]> {
getUniqueOSOfLastMonthOrWeek(collection: Collection<ISession>, { year, month, day, type = 'month' }: DestructuredDateWithType): Promise<ISession[]> {
return collection.aggregate([{
$match: {
type: 'user_daily',
@ -535,7 +535,7 @@ export const aggregates = {
}], { allowDiskUse: true }).toArray();
},
getUniqueOSOfYesterday(collection: Collection<T>, { year, month, day }: DestructuredDate): Promise<T[]> {
getUniqueOSOfYesterday(collection: Collection<ISession>, { year, month, day }: DestructuredDate): Promise<ISession[]> {
return collection.aggregate([{
$match: {
year,
@ -577,7 +577,7 @@ export const aggregates = {
},
};
export class SessionsRaw extends BaseRaw<T> {
export class SessionsRaw extends BaseRaw<ISession> {
protected indexes: IndexSpecification[] = [
{ key: { instanceId: 1, sessionId: 1, year: 1, month: 1, day: 1 } },
{ key: { instanceId: 1, sessionId: 1, userId: 1 } },
@ -590,19 +590,19 @@ export class SessionsRaw extends BaseRaw<T> {
{ key: { _computedAt: 1 }, expireAfterSeconds: 60 * 60 * 24 * 45 },
]
private secondaryCollection: Collection<T>;
private secondaryCollection: Collection<ISession>;
constructor(
public readonly col: Collection<T>,
public readonly colSecondary: Collection<T>,
public readonly trash?: Collection<T>,
public readonly col: Collection<ISession>,
public readonly colSecondary: Collection<ISession>,
public readonly trash?: Collection<ISession>,
) {
super(col, trash);
this.secondaryCollection = colSecondary;
}
async getActiveUsersBetweenDates({ start, end }: DestructuredRange): Promise<T[]> {
async getActiveUsersBetweenDates({ start, end }: DestructuredRange): Promise<ISession[]> {
return this.col.aggregate([
{
$match: {
@ -618,7 +618,7 @@ export class SessionsRaw extends BaseRaw<T> {
]).toArray();
}
async findLastLoginByIp(ip: string): Promise<T | null> {
async findLastLoginByIp(ip: string): Promise<ISession | null> {
return this.findOne({
ip,
}, {
@ -627,7 +627,7 @@ export class SessionsRaw extends BaseRaw<T> {
});
}
async getActiveUsersOfPeriodByDayBetweenDates({ start, end }: DestructuredRange): Promise<T[]> {
async getActiveUsersOfPeriodByDayBetweenDates({ start, end }: DestructuredRange): Promise<ISession[]> {
return this.col.aggregate([
{
$match: {
@ -675,7 +675,7 @@ export class SessionsRaw extends BaseRaw<T> {
]).toArray();
}
async getBusiestTimeWithinHoursPeriod({ start, end, groupSize }: DestructuredRange & {groupSize: number}): Promise<T[]> {
async getBusiestTimeWithinHoursPeriod({ start, end, groupSize }: DestructuredRange & {groupSize: number}): Promise<ISession[]> {
const match = {
$match: {
type: 'computed-session',
@ -709,7 +709,7 @@ export class SessionsRaw extends BaseRaw<T> {
return this.col.aggregate([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort]).toArray();
}
async getTotalOfSessionsByDayBetweenDates({ start, end }: DestructuredRange): Promise<T[]> {
async getTotalOfSessionsByDayBetweenDates({ start, end }: DestructuredRange): Promise<ISession[]> {
return this.col.aggregate([
{
$match: {
@ -739,7 +739,7 @@ export class SessionsRaw extends BaseRaw<T> {
]).toArray();
}
async getTotalOfSessionByHourAndDayBetweenDates({ start, end }: DestructuredRange): Promise<T[]> {
async getTotalOfSessionByHourAndDayBetweenDates({ start, end }: DestructuredRange): Promise<ISession[]> {
const match = {
$match: {
type: 'computed-session',
@ -922,7 +922,7 @@ export class SessionsRaw extends BaseRaw<T> {
};
}
async createOrUpdate(data: T): Promise<UpdateWriteOpResult | undefined> {
async createOrUpdate(data: ISession): Promise<UpdateWriteOpResult | undefined> {
const { year, month, day, sessionId, instanceId } = data;
if (!year || !month || !day || !sessionId || !instanceId) {
@ -992,12 +992,12 @@ export class SessionsRaw extends BaseRaw<T> {
return this.updateMany(query, update);
}
async createBatch(sessions: ModelOptionalId<T>[]): Promise<BulkWriteOpResultObject | undefined> {
async createBatch(sessions: ModelOptionalId<ISession>[]): Promise<BulkWriteOpResultObject | undefined> {
if (!sessions || sessions.length === 0) {
return;
}
const ops: BulkWriteOperation<T>[] = [];
const ops: BulkWriteOperation<ISession>[] = [];
sessions.forEach((doc) => {
const { year, month, day, sessionId, instanceId } = doc;
delete doc._id;

@ -0,0 +1,43 @@
import { Serialized } from '../../../../definition/Serialized';
export declare const APIClient: {
delete<P, R = any>(endpoint: string, params?: Serialized<P>): Promise<Serialized<R>>;
get<P, R = any>(endpoint: string, params?: Serialized<P>): Promise<Serialized<R>>;
post<P, B, R = any>(endpoint: string, params?: Serialized<P>, body?: B): Promise<Serialized<R>>;
upload<P, B, R = any>(
endpoint: string,
params?: Serialized<P>,
formData?: B,
xhrOptions?: {
progress: (amount: number) => void;
error: (ev: ProgressEvent<XMLHttpRequestEventTarget>) => void;
}
): { promise: Promise<Serialized<R>> };
getCredentials(): {
'X-User-Id': string;
'X-Auth-Token': string;
};
_jqueryCall(
method?: string,
endpoint?: string,
params?: any,
body?: any,
headers?: Record<string,
string>,
dataType?: string
): any;
v1: {
delete<P, R = any>(endpoint: string, params?: Serialized<P>): Promise<Serialized<R>>;
get<P, R = any>(endpoint: string, params?: Serialized<P>): Promise<Serialized<R>>;
post<P, B, R = any>(endpoint: string, params?: Serialized<P>, body?: B): Promise<Serialized<R>>;
upload<P, B, R = any>(
endpoint: string,
params?: Serialized<P>,
formData?: B,
xhrOptions?: {
progress: (amount: number) => void;
error: (ev: ProgressEvent<XMLHttpRequestEventTarget>) => void;
}
): { promise: Promise<Serialized<R>> };
};
};

@ -1,5 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';
import jQuery from 'jquery';
import { process2faReturn } from '../../../../client/lib/2fa/process2faReturn';
import { baseURI } from '../../../../client/lib/baseURI';

@ -1,8 +1,15 @@
import { createContext, useCallback, useContext, useMemo } from 'react';
import { IServerInfo } from '../../../definition/IServerInfo';
import type { IServerInfo } from '../../../definition/IServerInfo';
import type { Serialized } from '../../../definition/Serialized';
import type { PathFor, Params, Return, Method } from '../../../definition/rest';
import type {
Method,
PathFor,
OperationParams,
MatchPathPattern,
OperationResult,
PathPattern,
} from '../../../definition/rest';
import {
ServerMethodFunction,
ServerMethodName,
@ -18,11 +25,11 @@ type ServerContextValue = {
methodName: MethodName,
...args: ServerMethodParameters<MethodName>
) => Promise<ServerMethodReturn<MethodName>>;
callEndpoint: <M extends Method, P extends PathFor<M>>(
method: M,
path: P,
params: Params<M, P>[0],
) => Promise<Serialized<Return<M, P>>>;
callEndpoint: <TMethod extends Method, TPath extends PathFor<TMethod>>(
method: TMethod,
path: TPath,
params: Serialized<OperationParams<TMethod, MatchPathPattern<TPath>>>,
) => Promise<Serialized<OperationResult<TMethod, MatchPathPattern<TPath>>>>;
uploadToEndpoint: (endpoint: string, params: any, formData: any) => Promise<void>;
getStream: (
streamName: string,
@ -70,10 +77,16 @@ export const useMethod = <MethodName extends keyof ServerMethods>(
);
};
export const useEndpoint = <M extends 'GET' | 'POST' | 'DELETE', P extends PathFor<M>>(
method: M,
path: P,
): ((params: Params<M, P>[0]) => Promise<Serialized<Return<M, P>>>) => {
type EndpointFunction<TMethod extends Method, TPathPattern extends PathPattern> = (
params: void extends OperationParams<TMethod, TPathPattern>
? void
: Serialized<OperationParams<TMethod, TPathPattern>>,
) => Promise<Serialized<OperationResult<TMethod, TPathPattern>>>;
export const useEndpoint = <TMethod extends Method, TPath extends PathFor<TMethod>>(
method: TMethod,
path: TPath,
): EndpointFunction<TMethod, MatchPathPattern<TPath>> => {
const { callEndpoint } = useContext(ServerContext);
return useCallback((params) => callEndpoint(method, path, params), [callEndpoint, path, method]);

@ -1,16 +1,24 @@
import { useCallback } from 'react';
import { Serialized } from '../../definition/Serialized';
import { Method, Params, PathFor, Return } from '../../definition/rest';
import {
MatchPathPattern,
Method,
OperationParams,
OperationResult,
PathFor,
} from '../../definition/rest';
import { useEndpoint } from '../contexts/ServerContext';
import { useToastMessageDispatch } from '../contexts/ToastMessagesContext';
export const useEndpointAction = <M extends Method, P extends PathFor<M>>(
method: M,
path: P,
params: Params<M, P>[0] = {},
export const useEndpointAction = <TMethod extends Method, TPath extends PathFor<TMethod>>(
method: TMethod,
path: TPath,
params: Serialized<OperationParams<TMethod, MatchPathPattern<TPath>>> = {} as Serialized<
OperationParams<TMethod, MatchPathPattern<TPath>>
>,
successMessage?: string,
): ((extraParams?: Params<M, P>[1]) => Promise<Serialized<Return<M, P>>>) => {
): (() => Promise<Serialized<OperationResult<TMethod, MatchPathPattern<TPath>>>>) => {
const sendData = useEndpoint(method, path);
const dispatchToastMessage = useToastMessageDispatch();

@ -1,15 +1,26 @@
import { useCallback } from 'react';
import { Serialized } from '../../definition/Serialized';
import { Method, Params, PathFor, Return } from '../../definition/rest';
import {
MatchPathPattern,
Method,
OperationParams,
OperationResult,
PathFor,
} from '../../definition/rest';
import { useEndpoint } from '../contexts/ServerContext';
import { useToastMessageDispatch } from '../contexts/ToastMessagesContext';
export const useEndpointActionExperimental = <M extends Method, P extends PathFor<M>>(
method: M,
path: P,
export const useEndpointActionExperimental = <
TMethod extends Method,
TPath extends PathFor<TMethod>,
>(
method: TMethod,
path: TPath,
successMessage?: string,
): ((params: Params<M, P>[0]) => Promise<Serialized<Return<M, P>>>) => {
): ((
params: Serialized<OperationParams<TMethod, MatchPathPattern<TPath>>>,
) => Promise<Serialized<OperationResult<TMethod, MatchPathPattern<TPath>>>>) => {
const sendData = useEndpoint(method, path);
const dispatchToastMessage = useToastMessageDispatch();

@ -1,18 +1,26 @@
import { useCallback, useEffect } from 'react';
import { Serialized } from '../../definition/Serialized';
import { Params, PathFor, Return } from '../../definition/rest';
import { MatchPathPattern, OperationParams, OperationResult, PathFor } from '../../definition/rest';
import { useEndpoint } from '../contexts/ServerContext';
import { useToastMessageDispatch } from '../contexts/ToastMessagesContext';
import { AsyncState, useAsyncState } from './useAsyncState';
const defaultParams = {};
export const useEndpointData = <P extends PathFor<'GET'>>(
endpoint: P,
params: Params<'GET', P>[0] = defaultParams as Params<'GET', P>[0],
initialValue?: Serialized<Return<'GET', P>> | (() => Serialized<Return<'GET', P>>),
): AsyncState<Serialized<Return<'GET', P>>> & { reload: () => void } => {
export const useEndpointData = <TPath extends PathFor<'GET'>>(
endpoint: TPath,
params: void extends OperationParams<'GET', MatchPathPattern<TPath>>
? void
: Serialized<
OperationParams<'GET', MatchPathPattern<TPath>>
> = undefined as void extends OperationParams<'GET', MatchPathPattern<TPath>>
? void
: Serialized<OperationParams<'GET', MatchPathPattern<TPath>>>,
initialValue?:
| Serialized<OperationResult<'GET', MatchPathPattern<TPath>>>
| (() => Serialized<OperationResult<'GET', MatchPathPattern<TPath>>>),
): AsyncState<Serialized<OperationResult<'GET', MatchPathPattern<TPath>>>> & {
reload: () => void;
} => {
const { resolve, reject, reset, ...state } = useAsyncState(initialValue);
const dispatchToastMessage = useToastMessageDispatch();
const getData = useEndpoint('GET', endpoint);

@ -5,14 +5,39 @@ import { Users } from '../../app/models/client';
import { Notifications } from '../../app/notifications/client';
import { APIClient } from '../../app/utils/client';
import type { IUser, IUserDataEvent } from '../../definition/IUser';
import { Serialized } from '../../definition/Serialized';
export const isSyncReady = new ReactiveVar(false);
type RawUserData = Omit<IUser, '_updatedAt'> & {
_updatedAt: string;
};
type RawUserData = Serialized<
Pick<
IUser,
| '_id'
| 'type'
| 'name'
| 'username'
| 'emails'
| 'status'
| 'statusDefault'
| 'statusText'
| 'statusConnection'
| 'avatarOrigin'
| 'utcOffset'
| 'language'
| 'settings'
| 'roles'
| 'active'
| 'defaultRoom'
| 'customFields'
| 'statusLivechat'
| 'oauth'
| 'createdAt'
| '_updatedAt'
| 'avatarETag'
>
>;
const updateUser = (userData: IUser & { _updatedAt: Date }): void => {
const updateUser = (userData: IUser): void => {
const user: IUser = Users.findOne({ _id: userData._id });
if (!user || !user._updatedAt || user._updatedAt.getTime() < userData._updatedAt.getTime()) {
@ -57,6 +82,7 @@ export const synchronizeUserData = async (uid: Meteor.User['_id']): Promise<RawU
if (userData) {
updateUser({
...userData,
createdAt: new Date(userData.createdAt),
_updatedAt: new Date(userData._updatedAt),
});
}

@ -3,7 +3,13 @@ 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,
PathFor,
MatchPathPattern,
OperationParams,
OperationResult,
} from '../../definition/rest';
import {
ServerContext,
ServerMethodName,
@ -28,11 +34,11 @@ const callMethod = <MethodName extends ServerMethodName>(
});
});
const callEndpoint = <M extends Method, P extends PathFor<M>>(
method: M,
path: P,
params: Params<M, P>[0],
): Promise<Serialized<Return<M, P>>> => {
const callEndpoint = <TMethod extends Method, TPath extends PathFor<TMethod>>(
method: TMethod,
path: TPath,
params: Serialized<OperationParams<TMethod, MatchPathPattern<TPath>>>,
): Promise<Serialized<OperationResult<TMethod, MatchPathPattern<TPath>>>> => {
const api = path[0] === '/' ? APIClient : APIClient.v1;
const endpointPath = path[0] === '/' ? path.slice(1) : path;

@ -4,14 +4,15 @@ import { Tracker } from 'meteor/tracker';
import { Notifications } from '../../app/notifications/client';
import { APIClient } from '../../app/utils/client';
import { IBanner, BannerPlatform } from '../../definition/IBanner';
import { Serialized } from '../../definition/Serialized';
import * as banners from '../lib/banners';
const fetchInitialBanners = async (): Promise<void> => {
const response = (await APIClient.get('v1/banners', {
platform: BannerPlatform.Web,
})) as {
const response: Serialized<{
banners: IBanner[];
};
}> = await APIClient.get('v1/banners', {
platform: BannerPlatform.Web,
});
for (const banner of response.banners) {
banners.open({
@ -22,11 +23,11 @@ const fetchInitialBanners = async (): Promise<void> => {
};
const handleBanner = async (event: { bannerId: string }): Promise<void> => {
const response = (await APIClient.get(`v1/banners/${event.bannerId}`, {
platform: BannerPlatform.Web,
})) as {
const response: Serialized<{
banners: IBanner[];
};
}> = await APIClient.get(`v1/banners/${event.bannerId}`, {
platform: BannerPlatform.Web,
});
if (!response.banners.length) {
return banners.closeById(event.bannerId);

@ -39,7 +39,7 @@ function LDAPGroupPage({ _id, ...group }: ISetting): JSX.Element {
const handleTestConnectionButtonClick = async (): Promise<void> => {
try {
const { message } = await testConnection(undefined);
const { message } = await testConnection();
dispatchToastMessage({ type: 'success', message: t(message) });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
@ -48,12 +48,12 @@ function LDAPGroupPage({ _id, ...group }: ISetting): JSX.Element {
const handleSyncNowButtonClick = async (): Promise<void> => {
try {
await testConnection(undefined);
await testConnection();
const confirmSync = async (): Promise<void> => {
closeModal();
try {
const { message } = await syncNow(undefined);
const { message } = await syncNow();
dispatchToastMessage({ type: 'success', message: t(message) });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
@ -79,7 +79,7 @@ function LDAPGroupPage({ _id, ...group }: ISetting): JSX.Element {
const handleSearchTestButtonClick = async (): Promise<void> => {
try {
await testConnection(undefined);
await testConnection();
let username = '';
const handleChangeUsername = (event: FormEvent<HTMLInputElement>): void => {
username = event.currentTarget.value;

@ -21,9 +21,7 @@ export const useDepartmentsByUnitsList = (
} => {
const [itemsList, setItemsList] = useState(() => new RecordList<ILivechatDepartmentRecord>());
const reload = useCallback(() => setItemsList(new RecordList<ILivechatDepartmentRecord>()), []);
const endpoint = `livechat/departments.available-by-unit/${
options.unitId || 'none'
}` as 'livechat/departments.by-unit/';
const endpoint = `livechat/departments.available-by-unit/${options.unitId || 'none'}` as const;
const getDepartments = useEndpoint('GET', endpoint);

@ -1,9 +1,10 @@
export type Serialized<T> = T extends Date
? (Exclude<T, Date> | string)
: T extends boolean | number | string | null | undefined
? T
: T extends {}
? {
[K in keyof T]: Serialized<T[K]>;
}
: null;
export type Serialized<T> =
T extends Date
? (Exclude<T, Date> | string)
: T extends boolean | number | string | null | undefined
? T
: T extends {}
? {
[K in keyof T]: Serialized<T[K]>;
}
: null;

@ -0,0 +1,7 @@
import 'meteor/check';
declare module 'meteor/check' {
namespace Match {
function Where<T, U extends T>(condition: (val: T) => val is U): Matcher<U>;
}
}

@ -0,0 +1,8 @@
export type ReplacePlaceholders<TPath extends string> =
string extends TPath
? TPath
: TPath extends `${ infer Start }:${ infer _Param }/${ infer Rest }`
? `${ Start }${ string }/${ ReplacePlaceholders<Rest> }`
: TPath extends `${ infer Start }:${ infer _Param }`
? `${ Start }${ string }`
: TPath;

@ -1,7 +1,8 @@
import type { EnterpriseEndpoints } from '../../ee/definition/rest';
import type { ExtractKeys, ValueOf } from '../utils';
import type { KeyOfEach } from '../utils';
import type { AppsEndpoints } from './apps';
import { BannersEndpoints } from './v1/banners';
import type { ReplacePlaceholders } from './helpers/ReplacePlaceholders';
import type { BannersEndpoints } from './v1/banners';
import type { ChannelsEndpoints } from './v1/channels';
import type { ChatEndpoints } from './v1/chat';
import type { CloudEndpoints } from './v1/cloud';
@ -11,20 +12,21 @@ 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 { 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 { PermissionsEndpoints } from './v1/permissions';
import type { RolesEndpoints } from './v1/roles';
import type { RoomsEndpoints } from './v1/rooms';
import { SettingsEndpoints } from './v1/settings';
import type { 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 &
type CommunityEndpoints = BannersEndpoints &
ChatEndpoints &
ChannelsEndpoints &
CloudEndpoints &
CustomUserStatusEndpoints &
@ -47,51 +49,76 @@ MiscEndpoints &
PermissionsEndpoints &
InstancesEndpoints;
export type Endpoints = CommunityEndpoints & EnterpriseEndpoints;
type Endpoints = CommunityEndpoints & EnterpriseEndpoints;
type Endpoint = UnionizeEndpoints<Endpoints>;
type OperationsByPathPattern<TPathPattern extends keyof Endpoints> = TPathPattern extends any
? OperationsByPathPatternAndMethod<TPathPattern>
: never;
type UnionizeEndpoints<EE extends Endpoints> = ValueOf<
{
[P in keyof EE]: UnionizeMethods<P, EE[P]>;
}
>;
type OperationsByPathPatternAndMethod<
TPathPattern extends keyof Endpoints,
TMethod extends KeyOfEach<Endpoints[TPathPattern]> = KeyOfEach<Endpoints[TPathPattern]>
> = TMethod extends any
? {
pathPattern: TPathPattern;
method: TMethod;
path: ReplacePlaceholders<TPathPattern>;
params: GetParams<Endpoints[TPathPattern][TMethod]>;
result: GetResult<Endpoints[TPathPattern][TMethod]>;
}
: never;
type ExtractOperations<OO, M extends keyof OO> = ExtractKeys<OO, M, (...args: any[]) => any>;
type Operations = OperationsByPathPattern<keyof Endpoints>;
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 PathPattern = Operations['pathPattern'];
export type Method = Parameters<Endpoint>[0];
export type Path = Parameters<Endpoint>[1];
export type Method = Operations['method'];
export type MethodFor<P extends Path> = P extends any
? Parameters<Extract<Endpoint, (method: any, path: P, ...params: any[]) => any>>[0]
export type Path = Operations['path'];
export type MethodFor<TPath extends Path> = TPath extends any
? Extract<Operations, { path: TPath }>['method']
: never;
export type PathFor<M extends Method> = M extends any
? Parameters<Extract<Endpoint, (method: M, path: any, ...params: any[]) => any>>[1]
export type PathFor<TMethod extends Method> = TMethod extends any
? Extract<Operations, { method: TMethod }>['path']
: 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
export type MatchPathPattern<TPath extends Path> = TPath extends any
? Extract<Operations, { path: TPath }>['pathPattern']
: never;
type ExtractParams<Q> = Q extends [any, any]
? [undefined?]
: Q extends [any, any, any, ...any[]]
? [Q[2]]
export type JoinPathPattern<TBasePath extends string, TSubPathPattern extends string> = Extract<
PathPattern,
`${ TBasePath }/${ TSubPathPattern }` | TSubPathPattern
>;
type GetParams<TOperation> = TOperation extends (...args: any) => any
? Parameters<TOperation>[0] extends void ? void : Parameters<TOperation>[0]
: never
type GetResult<TOperation> = TOperation extends (...args: any) => any
? ReturnType<TOperation>
: never
export type OperationParams<TMethod extends Method, TPathPattern extends PathPattern> =
TMethod extends keyof Endpoints[TPathPattern]
? GetParams<Endpoints[TPathPattern][TMethod]>
: 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>>;
export type OperationResult<TMethod extends Method, TPathPattern extends PathPattern> =
TMethod extends keyof Endpoints[TPathPattern]
? GetResult<Endpoints[TPathPattern][TMethod]>
: never;
export type UrlParams<T extends string> = string extends T
? Record<string, string>
: T extends `${ infer _Start }:${ infer Param }/${ infer Rest }`
? { [k in Param | keyof UrlParams<Rest>]: string }
: T extends `${ infer _Start }:${ infer Param }`
? { [k in Param]: string }
: {};
export type MethodOf<TPathPattern extends PathPattern> = TPathPattern extends any
? keyof Endpoints[TPathPattern]
: never;

@ -1,21 +1,21 @@
import { IBanner } from '../../IBanner';
import type { BannerPlatform, IBanner } from '../../IBanner';
export type BannersEndpoints = {
/* @deprecated */
'banners.getNew': {
GET: () => ({
GET: (params: { platform: BannerPlatform; bid: IBanner['_id'] }) => ({
banners: IBanner[];
});
};
'banners/:id': {
GET: (params: { platform: string }) => ({
GET: (params: { platform: BannerPlatform }) => ({
banners: IBanner[];
});
};
'banners': {
GET: () => ({
GET: (params: { platform: BannerPlatform }) => ({
banners: IBanner[];
});
};

@ -49,13 +49,17 @@ export type OmnichannelEndpoints = {
};
};
'livechat/department/:_id': {
path: `livechat/department/${ string }`;
GET: () => {
department: ILivechatDepartment;
};
};
'livechat/departments.by-unit/': {
path: `livechat/departments.by-unit/${ string }`;
'livechat/departments.by-unit/:id': {
GET: (params: { text: string; offset: number; count: number }) => {
departments: ILivechatDepartment[];
total: number;
};
};
'livechat/departments.available-by-unit/:id': {
GET: (params: { text: string; offset: number; count: number }) => {
departments: ILivechatDepartment[];
total: number;
@ -139,7 +143,6 @@ export type OmnichannelEndpoints = {
DELETE: (params: { _id: IOmnichannelCannedResponse['_id'] }) => void;
};
'canned-responses/:_id': {
path: `canned-responses/${ string }`;
GET: () => {
cannedResponse: IOmnichannelCannedResponse;
};

@ -109,6 +109,7 @@ type RoleRemoveUserFromRoleProps = {
username: string;
roleName: string;
roomId?: string;
scope?: string;
}
const roleRemoveUserFromRolePropsSchema: JSONSchemaType<RoleRemoveUserFromRoleProps> = {
@ -124,6 +125,10 @@ const roleRemoveUserFromRolePropsSchema: JSONSchemaType<RoleRemoveUserFromRolePr
type: 'string',
nullable: true,
},
scope: {
type: 'string',
nullable: true,
},
},
required: ['username', 'roleName'],
additionalProperties: false,

@ -1,19 +1,17 @@
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';
import type { PaginatedResult } from '../../helpers/PaginatedResult';
import type { PaginatedRequest } from '../../helpers/PaginatedRequest';
import type { ITeamAutocompleteResult, ITeamMemberInfo } from '../../../../server/sdk/types/ITeamService';
import type { TeamsRemoveRoomProps } from './TeamsRemoveRoomProps';
import type { TeamsConvertToChannelProps } from './TeamsConvertToChannelProps';
import type { TeamsUpdateMemberProps } from './TeamsUpdateMemberProps';
import type { TeamsAddMembersProps } from './TeamsAddMembersProps';
import type { TeamsRemoveMemberProps } from './TeamsRemoveMemberProps';
import type { TeamsDeleteProps } from './TeamsDeleteProps';
import type { TeamsLeaveProps } from './TeamsLeaveProps';
import type { TeamsUpdateProps } from './TeamsUpdateProps';
type TeamProps =
@ -40,7 +38,7 @@ export type TeamsEndpoints = {
'teams.create': {
POST: (params: {
name: ITeam['name'];
type?: ITeam['type'];
type: ITeam['type'];
members?: IUser['_id'][];
room: {
id?: string;
@ -138,7 +136,7 @@ export type TeamsEndpoints = {
};
'teams.listRooms': {
GET: (params: PaginatedRequest & ({ teamId: string } | { teamId: string }) & { filter?: string; type?: string }) => PaginatedResult & { rooms: IRoom[] };
GET: (params: PaginatedRequest & ({ teamId: string } | { teamName: string }) & { filter?: string; type?: string }) => PaginatedResult & { rooms: IRoom[] };
};

@ -9,3 +9,6 @@ export type UnionToIntersection<T> = (T extends any ? (x: T) => void : never) ex
: never;
export type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
// `T extends any` is a trick to apply a operator to each member of a union
export type KeyOfEach<T> = T extends any ? keyof T : never;

@ -2,6 +2,16 @@ import { ILivechatBusinessHour } from '../../../../../definition/ILivechatBusine
export type OmnichannelBusinessHoursEndpoints = {
'livechat/business-hours.list': {
GET: () => ({ businessHours: ILivechatBusinessHour[] });
GET: (params: {
name?: string;
offset: number;
count: number;
sort: Record<string, unknown>;
}) => {
businessHours: ILivechatBusinessHour[];
count: number;
offset: number;
total: number;
};
};
}

@ -41,7 +41,7 @@ export interface ITeamInfo extends ITeam {
}
export interface IListRoomsFilter {
name: string;
name?: string;
isDefault: boolean;
getAllRooms: boolean;
allowPrivateTeam: boolean;

@ -27,7 +27,6 @@ describe('banners', function() {
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'Match error: Missing key \'platform\'');
})
.end(done);
});
@ -41,8 +40,6 @@ describe('banners', function() {
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'Platform is unknown. [error-unknown-platform]');
expect(res.body).to.have.property('errorType', 'error-unknown-platform');
})
.end(done);
});
@ -56,8 +53,6 @@ describe('banners', function() {
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'The required "platform" param is missing. [error-missing-param]');
expect(res.body).to.have.property('errorType', 'error-missing-param');
})
.end(done);
});
@ -112,8 +107,6 @@ describe('banners', function() {
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
expect(res.body).to.have.property('error', 'The required "bannerId" param is missing. [error-missing-param]');
expect(res.body).to.have.property('errorType', 'error-missing-param');
})
.end(done);
});

Loading…
Cancel
Save