Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions .changeset/migrate-rooms-favorite-endpoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Migrated rooms.favorite endpoint to new OpenAPI pattern with AJV validation
52 changes: 37 additions & 15 deletions apps/meteor/app/api/server/v1/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import {
findChannelAndPrivateAutocompleteWithPagination,
findRoomsAvailableForTeams,
} from '../lib/rooms';
import { required } from 'zod/mini';

export async function findRoomByIdOrName({
params,
Expand Down Expand Up @@ -311,25 +312,46 @@ API.v1.addRoute(
},
);

API.v1.addRoute(
'rooms.favorite',
{ authRequired: true },
{
async post() {
const { favorite } = this.bodyParams;
export const roomsFavoriteEndpoint = API.v1.post('rooms.favorite', {
authRequired: true,
body: ajv.compile<{ favorite: boolean} & ({ roomId: string} | { roomName: string })>({
type: 'object',
properties: {
roomId: { type: 'string', minLength: 1 },
roomName: { type: 'string', minLength: 1 },
favorite: { type: 'boolean' },
},
oneOf: [
{ required: ['roomId', 'favorite'] },
{ required: ['roomName', 'favorite'] },
],
additionalProperties: false
}),
response: {
200: ajv.compile({
type: 'object',
properties: { success: { type: 'boolean', enum: [true] } },
required: ['success'],
additionalProperties: false,
}),
400: validateBadRequestErrorResponse,
401: validateUnauthorizedErrorResponse,
}
},
async function action() {
const { favorite } = this.bodyParams;
const room = await findRoomByIdOrName({ params: this.bodyParams });

if (!this.bodyParams.hasOwnProperty('favorite')) {
return API.v1.failure("The 'favorite' param is required");
}
await toggleFavoriteMethod(this.userId, room._id, favorite);
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

findRoomByIdOrName with the default checkedArchived: true may silently block favoriting archived rooms.

The old endpoint did not go through findRoomByIdOrName (per the AI summary, room resolution "now" happens here), so users who had archived rooms in their favorites could previously still toggle them. With checkedArchived: true the handler now throws error-room-archived before reaching toggleFavoriteMethod, making the behavior change inconsistent with the PR claim that "business logic is unchanged."

If archiving a room should not prevent it from being un-favorited, pass checkedArchived: false:

🐛 Proposed fix
-	const room = await findRoomByIdOrName({ params: this.bodyParams });
+	const room = await findRoomByIdOrName({ params: this.bodyParams, checkedArchived: false });
#!/bin/bash
# Check what the old rooms.favorite addRoute handler did before this PR (git blame / old code search)
rg -n 'rooms\.favorite' apps/meteor/app/api/server/v1/rooms.ts
rg -n 'toggleFavorite' apps/meteor/app/api/server/v1/rooms.ts
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/meteor/app/api/server/v1/rooms.ts` around lines 341 - 345, The handler
now calls findRoomByIdOrName with its default checkedArchived:true which causes
archived rooms to throw error-room-archived and prevents toggleFavoriteMethod
from running; update the action function to call findRoomByIdOrName with
checkedArchived:false so archived rooms can still be unfavorited (i.e., pass {
params: this.bodyParams, checkedArchived: false } to findRoomByIdOrName) while
leaving toggleFavoriteMethod(this.userId, room._id, favorite) unchanged.


const room = await findRoomByIdOrName({ params: this.bodyParams });
return API.v1.success();
});

await toggleFavoriteMethod(this.userId, room._id, favorite);
type RoomsFavoriteEndpoint = ExtractRoutesFromAPI<typeof roomsFavoriteEndpoint>;

return API.v1.success();
},
},
);
declare module '@rocket.chat/rest-typings' {
interface Endpoints extends RoomsFavoriteEndpoint {}
}

API.v1.addRoute(
'rooms.cleanHistory',
Expand Down
14 changes: 0 additions & 14 deletions packages/rest-typings/src/v1/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -805,20 +805,6 @@ export type RoomsEndpoints = {
POST: (params: { roomId: string; notifications: Notifications }) => void;
};

'/v1/rooms.favorite': {
POST: (
params:
| {
roomId: string;
favorite: boolean;
}
| {
roomName: string;
favorite: boolean;
},
) => void;
};

'/v1/rooms.nameExists': {
GET: (params: { roomName: string }) => {
exists: boolean;
Expand Down