Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
7d566b8
fix(api): update forbidden method signature to support optional messa…
ggazzo Mar 23, 2026
5cd61bd
feat(rest-typings): upgrade Ajv to 2020 draft for unevaluatedProperties
ggazzo Mar 20, 2026
41187a5
feat(core-typings): add IMeApiUser and update ISubscription, IIntegra…
ggazzo Mar 20, 2026
242fc8b
fix(client): avoid defaulting email2fa.changedAt when /me omits it
ggazzo Mar 20, 2026
aaba15e
refactor(api): use $ref schemas in response validation
ggazzo Mar 20, 2026
4a86f07
chore(api): migrate info, stats, mailer endpoints to OpenAPI
ggazzo Mar 20, 2026
8aba6e4
chore(api): migrate push, subscriptions endpoints to OpenAPI
ggazzo Mar 20, 2026
f1cbf46
fix(api): include push.get/push.info in ExtractRoutesFromAPI
ggazzo Mar 20, 2026
b10deb9
chore(api): migrate invites, custom-user-status, emoji-custom endpoin…
ggazzo Mar 20, 2026
aed58f5
fix(api): validate emoji-custom.all emojis with IEmojiCustom AJV schema
ggazzo Mar 20, 2026
164cd49
fix(api): require non-empty emojiId for emoji-custom.delete
ggazzo Mar 20, 2026
8e0b79e
chore(authentication): remove debug console.log from onCreateUserAsync
ggazzo Mar 20, 2026
31fe74b
chore(api): migrate commands.run and commands.preview to OpenAPI
ggazzo Mar 20, 2026
15f0be8
fix: handle type mismatches found during OpenAPI migration
ggazzo Mar 20, 2026
2f15c41
refactor(tests): update test assertions for OpenAPI migration
ggazzo Mar 20, 2026
0e35955
chore: add scripts for tracking API migration progress
ggazzo Mar 20, 2026
900cb96
docs: update API endpoint migration tracking documentation
ggazzo Mar 20, 2026
573e8e7
fix(api): resolve duplicate import warnings in ApiClass.ts
ggazzo Mar 23, 2026
555bf93
chore(api): migrate commands.list to OpenAPI
ggazzo Mar 20, 2026
9c6695b
chore(api): migrate dm/im.create, dm/im.open, dm/im.setTopic to OpenAPI
ggazzo Mar 20, 2026
4c4e71f
chore(api): migrate dm/im.counters to OpenAPI
ggazzo Mar 20, 2026
d874a1a
chore(api): migrate dm/im.files, dm/im.history, dm/im.members, dm/im.…
ggazzo Mar 20, 2026
df2168e
chore(api): migrate chat.react, chat.reportMessage to OpenAPI
ggazzo Mar 20, 2026
a82df15
chore(api): migrate shield.svg to OpenAPI
ggazzo Mar 20, 2026
b9186b3
fix: restore endpoint types in rest-typings for external package cons…
ggazzo Mar 22, 2026
2b21847
fix(api): add query validator to commands.list for proper type extrac…
ggazzo Mar 23, 2026
1a03d12
refactor(api): Lint
ggazzo Mar 23, 2026
5c90cd8
docs: add pitfalls for rest-typings removal and as const usage
ggazzo Mar 23, 2026
5465a65
lint
ggazzo Mar 23, 2026
b395812
fix(api): use forbidden() without args to preserve original error mes…
ggazzo Mar 23, 2026
a7f20f0
fix(tests): update invite-all-from test to expect 'unauthorized' error
ggazzo Mar 23, 2026
ca17e18
refactor(api): migrate rooms (#39825)
ggazzo Mar 23, 2026
2336ba3
chore(api): migrate all teams endpoints (#39824)
ggazzo Mar 23, 2026
c3eb913
chore(api): migrate chat endpoints to OpenAPI (#39820)
ggazzo Mar 24, 2026
932aa61
fix(api): convert room apis (#39827)
ggazzo Mar 24, 2026
85d0087
chore(api): convert user apis (#39843)
ggazzo Mar 25, 2026
007dd57
chore: api convertion review (#39851)
ggazzo Mar 25, 2026
40ff9cc
chore(api): address PR review issues for OpenAPI endpoint migration (…
ggazzo Mar 26, 2026
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> {
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,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see isOpenAPIJSONEndpoint is using ajv instead of ajvQuery to compile the schema in packages/rest-typings/src/default/index.ts Is that okay or we should change it?

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
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 ?? new Date(0), lm);
unreadsFrom = subscription.ls || subscription.ts;
userMentions = subscription.userMentions;
joined = true;
Comment on lines 642 to 646
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how groups.ts handles the same scenario
rg -n "countVisibleByRoomIdBetweenTimestampsInclusive" apps/meteor/app/api/server/v1/groups.ts -B2 -A2

Repository: RocketChat/Rocket.Chat

Length of output: 350


Replace new Date(0) with subscription.ts for unread count calculation.

The fallback value new Date(0) counts all messages from Unix epoch as unread when subscription.ls is undefined. groups.ts (line 294) uses subscription.ls || subscription.ts instead. For consistency and correctness, apply the same fallback:

Suggested change
-unreads = await Messages.countVisibleByRoomIdBetweenTimestampsInclusive(subscription.rid, subscription.ls ?? new Date(0), lm);
+unreads = await Messages.countVisibleByRoomIdBetweenTimestampsInclusive(subscription.rid, subscription.ls ?? subscription.ts, lm);

Line 644 already uses subscription.ls || subscription.ts for unreadsFrom, so aligning this calculation removes the inconsistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/meteor/app/api/server/v1/channels.ts` around lines 642 - 646, The unread
count uses new Date(0) as a fallback which causes all messages since epoch to be
counted; change the fallback to subscription.ts to match the logic used for
unreadsFrom. Update the call to
Messages.countVisibleByRoomIdBetweenTimestampsInclusive so its start timestamp
uses subscription.ls || subscription.ts (consistent with the assignment of
unreadsFrom and with groups.ts logic), leaving other variables (unreadsFrom,
userMentions, joined) unchanged.

Expand Down
Loading
Loading