Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions apps/meteor/app/api/server/ApiClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import type {
UnavailableResult,
GenericRouteExecutionContext,
TooManyRequestsResult,
SuccessStatusCodes,
} from './definition';
import { getUserInfo } from './helpers/getUserInfo';
import { parseJsonQuery } from './helpers/parseJsonQuery';
Expand Down Expand Up @@ -266,15 +267,15 @@ export class APIClass<TBasePath extends string = '', TOperations extends Record<

public success(): SuccessResult<void>;

public success<T>(result: T): SuccessResult<T>;
public success<T>(result: T, statusCode?: SuccessStatusCodes): SuccessResult<T>;

public success<T>(result: T = {} as T): SuccessResult<T> {
public success<T>(result: T = {} as T, statusCode: SuccessStatusCodes = 200): SuccessResult<T> {
if (isObject(result)) {
(result as Record<string, any>).success = true;
}

const finalResult = {
statusCode: 200,
statusCode,
body: result,
} as SuccessResult<T>;

Expand All @@ -288,6 +289,8 @@ export class APIClass<TBasePath extends string = '', TOperations extends Record<
};
}

public failure(): FailureResult<string>;

public failure<T>(result?: T): FailureResult<T>;

public failure<T, TErrorType extends string, TStack extends string, TErrorDetails>(
Expand Down Expand Up @@ -363,6 +366,10 @@ export class APIClass<TBasePath extends string = '', TOperations extends Record<
};
}

public unauthorized(): UnauthorizedResult<string>;

public unauthorized<T>(msg: T): UnauthorizedResult<T>;

public unauthorized<T>(msg?: T): UnauthorizedResult<T> {
return {
statusCode: 401,
Expand All @@ -373,7 +380,11 @@ export class APIClass<TBasePath extends string = '', TOperations extends Record<
};
}

public forbidden<T = string>(msg?: T): ForbiddenResult<T> {
public forbidden(): ForbiddenResult<string>;

public forbidden<T>(msg: T): ForbiddenResult<T>;

public forbidden<T>(msg?: T): ForbiddenResult<T> {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return {
statusCode: 403,
body: {
Expand Down
7 changes: 7 additions & 0 deletions apps/meteor/app/api/server/ajv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import { ajv, ajvQuery } from '@rocket.chat/rest-typings';

const components = schemas.components?.schemas;
if (components) {
// Patch MessageAttachmentDefault to reject unknown properties so the oneOf
// discriminator works correctly (otherwise it matches every attachment).
const mad = components.MessageAttachmentDefault;
if (mad && typeof mad === 'object' && 'type' in mad) {
(mad as Record<string, unknown>).additionalProperties = false;
}

for (const key in components) {
if (Object.prototype.hasOwnProperty.call(components, key)) {
const uri = `#/components/schemas/${key}`;
Expand Down
24 changes: 20 additions & 4 deletions apps/meteor/app/api/server/default/info.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
import type { IWorkspaceInfo } from '@rocket.chat/core-typings';
import { ajv } from '@rocket.chat/rest-typings';

import { API } from '../api';
import { getServerInfo } from '../lib/getServerInfo';

API.default.addRoute(
const infoResponseSchema = ajv.compile<IWorkspaceInfo>({
type: 'object',
properties: {
version: { type: 'string' },
success: { type: 'boolean', enum: [true] },
},
required: ['success'],
additionalProperties: true,
});

API.default.get(
'info',
{ authRequired: false },
{
async get() {
return API.v1.success(await getServerInfo(this.userId));
authRequired: false,
response: {
200: infoResponseSchema,
},
},
async function action() {
return API.v1.success(await getServerInfo(this.userId));
},
);
33 changes: 26 additions & 7 deletions apps/meteor/app/api/server/default/openApi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { schemas } from '@rocket.chat/core-typings';
import type { Route } from '@rocket.chat/http-router';
import { isOpenAPIJSONEndpoint } from '@rocket.chat/rest-typings';
import { ajv, isOpenAPIJSONEndpoint } from '@rocket.chat/rest-typings';
import express from 'express';
import { WebApp } from 'meteor/webapp';
import swaggerUi from 'swagger-ui-express';
Expand Down Expand Up @@ -72,16 +72,35 @@ const makeOpenAPIResponse = (paths: Record<string, Record<string, Route>>) => ({
paths,
});

API.default.addRoute(
const openApiResponseSchema = ajv.compile<Record<string, unknown>>({
type: 'object',
properties: {
openapi: { type: 'string' },
info: { type: 'object' },
servers: { type: 'array' },
components: { type: 'object' },
paths: { type: 'object' },
schemas: { type: 'object' },
success: { type: 'boolean', enum: [true] },
},
required: ['openapi', 'info', 'paths', 'success'],
additionalProperties: false,
});

API.default.get(
'docs/json',
{ authRequired: false, validateParams: isOpenAPIJSONEndpoint },
{
get() {
const { withUndocumented = false } = this.queryParams;

return API.default.success(makeOpenAPIResponse(getTypedRoutes(API.api.typedRoutes, { withUndocumented })));
authRequired: false,
query: isOpenAPIJSONEndpoint,
Comment thread
ggazzo marked this conversation as resolved.
response: {
200: openApiResponseSchema,
},
},
function action() {
const { withUndocumented = false } = this.queryParams;

return API.default.success(makeOpenAPIResponse(getTypedRoutes(API.api.typedRoutes, { withUndocumented })));
},
);

app.use(
Expand Down
10 changes: 10 additions & 0 deletions apps/meteor/app/api/server/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,16 @@ export type TypedThis<TOptions extends TypedOptions, TPath extends string = ''>
requestIp?: string;
route: string;
response: Response;
readonly queryOperations: TOptions extends { queryOperations: infer T } ? T : never;
readonly queryFields: TOptions extends { queryFields: infer T } ? T : never;
readonly connection: {
token: string;
id: string;
close: () => void;
clientAddress: string;
httpHeaders: Record<string, string>;
};
readonly twoFactorChecked: boolean;
} & (TOptions['authRequired'] extends true
? {
user: TOptions extends { userWithoutUsername: true } ? IUser : RequiredField<IUser, 'username'>;
Expand Down
19 changes: 6 additions & 13 deletions apps/meteor/app/api/server/helpers/getUserInfo.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isOAuthUser, type IUser, type IUserEmail, type IUserCalendar } from '@rocket.chat/core-typings';
import { isOAuthUser, type IMeApiUser, type IUser, type IUserEmail, type IUserCalendar } from '@rocket.chat/core-typings';
import semver from 'semver';

import { settings } from '../../../settings/server';
Expand Down Expand Up @@ -84,15 +84,7 @@ const getUserCalendar = (email: false | IUserEmail | undefined): IUserCalendar =
return calendarSettings;
};

export async function getUserInfo(
me: IUser,
pullPreferences = true,
): Promise<
IUser & {
email?: string;
avatarUrl: string;
}
> {
export async function getUserInfo(me: IUser, pullPreferences = true): Promise<IMeApiUser> {
const verifiedEmail = isVerifiedEmail(me);

const userPreferences = me.settings?.preferences ?? {};
Expand All @@ -110,8 +102,8 @@ export async function getUserInfo(
isOAuthUser: isOAuthUser(me),
...(me.services && {
services: {
...(me.services.github && { github: me.services.github }),
...(me.services.gitlab && { gitlab: me.services.gitlab }),
...(me.services.github && { github: me.services.github as Record<string, unknown> }),
...(me.services.gitlab && { gitlab: me.services.gitlab as Record<string, unknown> }),
...(me.services.email2fa?.enabled && { email2fa: { enabled: me.services.email2fa.enabled } }),
...(me.services.totp?.enabled && { totp: { enabled: me.services.totp.enabled } }),
password: {
Expand All @@ -120,5 +112,6 @@ export async function getUserInfo(
},
},
}),
};
// Cast needed: spread of full IUser produces a superset; runtime response schema validates the actual shape
} as IMeApiUser;
}
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/lib/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export async function findChannelAndPrivateAutocompleteWithPagination({
};
}

export async function findRoomsAvailableForTeams({ uid, name }: { uid: string; name: string }): Promise<{
export async function findRoomsAvailableForTeams({ uid, name }: { uid: string; name?: string }): Promise<{
items: IRoom[];
}> {
const options: FindOptions<IRoom> = {
Expand Down
10 changes: 5 additions & 5 deletions apps/meteor/app/api/server/lib/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,11 @@ type FindPaginatedUsersByStatusProps = {
offset: number;
count: number;
sort: Record<string, 1 | -1>;
status: 'active' | 'deactivated';
roles: string[] | null;
searchTerm: string;
hasLoggedIn: boolean;
type: string;
status?: 'active' | 'deactivated';
roles?: string[] | null;
searchTerm?: string;
hasLoggedIn?: boolean;
type?: string;
inactiveReason?: ('deactivated' | 'pending_approval' | 'idle_too_long')[];
};

Expand Down
16 changes: 4 additions & 12 deletions apps/meteor/app/api/server/v1/autotranslate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,24 +91,16 @@ const autotranslateEndpoints = API.v1
return API.v1.failure('AutoTranslate is disabled.');
}

if (!roomId) {
return API.v1.failure('The bodyParam "roomId" is required.');
}
if (!field) {
return API.v1.failure('The bodyParam "field" is required.');
}
if (value === undefined) {
return API.v1.failure('The bodyParam "value" is required.');
}
if (field === 'autoTranslate' && typeof value !== 'boolean') {
// ajv 2020-12 with coerceTypes coerces booleans to strings, so check for both
if (field === 'autoTranslate' && value !== true && value !== 'true' && value !== false && value !== 'false') {
return API.v1.failure('The bodyParam "autoTranslate" must be a boolean.');
}

if (field === 'autoTranslateLanguage' && (typeof value !== 'string' || !Number.isNaN(Number.parseInt(value)))) {
if (field === 'autoTranslateLanguage' && typeof value === 'string' && !Number.isNaN(Number.parseInt(value))) {
return API.v1.failure('The bodyParam "autoTranslateLanguage" must be a string.');
}

await saveAutoTranslateSettings(this.userId, roomId, field, value === true ? '1' : String(value).valueOf(), {
await saveAutoTranslateSettings(this.userId, roomId, field, value === true || value === 'true' ? '1' : String(value), {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
defaultLanguage: defaultLanguage || '',
});

Expand Down
20 changes: 5 additions & 15 deletions apps/meteor/app/api/server/v1/call-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { getPaginationItems } from '../helpers/getPaginationItems';
type CallHistoryList = PaginatedRequest<{
filter?: string;
direction?: CallHistoryItem['direction'];
state?: CallHistoryItemState[] | CallHistoryItemState;
state?: CallHistoryItemState[];
}>;

const CallHistoryListSchema = {
Expand All @@ -42,20 +42,10 @@ const CallHistoryListSchema = {
enum: ['inbound', 'outbound'],
},
state: {
// our clients serialize arrays as `state=value1&state=value2`, but if there's a single value the parser doesn't know it is an array, so we need to support both arrays and direct values
// if a client tries to send a JSON array, our parser will treat it as a string and the type validation will reject it
// This means this param won't work from Swagger UI
oneOf: [
{
type: 'array',
items: {
$ref: '#/components/schemas/CallHistoryItemState',
},
},
{
$ref: '#/components/schemas/CallHistoryItemState',
},
],
type: 'array',
items: {
$ref: '#/components/schemas/CallHistoryItemState',
},
},
},
required: [],
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/v1/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ API.v1.addRoute(
const lm = room.lm ? room.lm : room._updatedAt;

if (subscription?.open) {
unreads = await Messages.countVisibleByRoomIdBetweenTimestampsInclusive(subscription.rid, subscription.ls, lm);
unreads = await Messages.countVisibleByRoomIdBetweenTimestampsInclusive(subscription.rid, subscription.ls ?? subscription.ts, lm);
unreadsFrom = subscription.ls || subscription.ts;
userMentions = subscription.userMentions;
joined = true;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down
Loading
Loading