Skip to content

Commit dbe0674

Browse files
Merge branch 'develop' into feat/add-endpoints-groups.membersOrderedByRole-channels.membersOrderedByRole
2 parents 7ee0f4b + 3c237b2 commit dbe0674

File tree

66 files changed

+2047
-1290
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+2047
-1290
lines changed

.changeset/eight-humans-sip.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@rocket.chat/meteor": minor
3+
"@rocket.chat/i18n": minor
4+
"@rocket.chat/rest-typings": minor
5+
---
6+
7+
Allows agents and managers to close Omnichannel rooms that for some reason ended up in a bad state. This "bad state" could be a room that appears open but it's closed. Now, the endpoint `livechat/room.closeByUser` will accept an optional `forceClose` parameter that will allow users to bypass most state checks we do on rooms and perform the room closing again so its state can be recovered.

.changeset/itchy-pumas-laugh.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@rocket.chat/meteor": patch
3+
---
4+
5+
Fixes SAML login redirecting to wrong room when using an invite link.

.changeset/metal-pets-promise.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@rocket.chat/meteor": minor
3+
"@rocket.chat/i18n": minor
4+
---
5+
6+
Fixes an issue where users without the "Preview public channel" permission would receive new messages sent to the channel

.yarn/releases/yarn-4.5.3.cjs .yarn/releases/yarn-4.6.0.cjs

+287-287
Large diffs are not rendered by default.

.yarnrc.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ plugins:
1212
- path: .yarn/plugins/@yarnpkg/plugin-engines.cjs
1313
spec: "https://raw.githubusercontent.com/devoto13/yarn-plugin-engines/main/bundles/%40yarnpkg/plugin-engines.js"
1414

15-
yarnPath: .yarn/releases/yarn-4.5.3.cjs
15+
yarnPath: .yarn/releases/yarn-4.6.0.cjs

apps/meteor/app/lib/server/functions/closeLivechatRoom.ts

+18-16
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import type { IUser, IRoom, IOmnichannelRoom } from '@rocket.chat/core-typings';
2-
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
32
import { LivechatRooms, Subscriptions } from '@rocket.chat/models';
43

54
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
65
import { Livechat } from '../../../livechat/server/lib/LivechatTyped';
76
import type { CloseRoomParams } from '../../../livechat/server/lib/localTypes';
8-
import { notifyOnSubscriptionChanged } from '../lib/notifyListener';
97

108
export const closeLivechatRoom = async (
119
user: IUser,
@@ -15,6 +13,7 @@ export const closeLivechatRoom = async (
1513
tags,
1614
generateTranscriptPdf,
1715
transcriptEmail,
16+
forceClose = false,
1817
}: {
1918
comment?: string;
2019
tags?: string[];
@@ -27,25 +26,14 @@ export const closeLivechatRoom = async (
2726
sendToVisitor: true;
2827
requestData: Pick<NonNullable<IOmnichannelRoom['transcriptRequest']>, 'email' | 'subject'>;
2928
};
29+
forceClose?: boolean;
3030
},
3131
): Promise<void> => {
3232
const room = await LivechatRooms.findOneById(roomId);
33-
if (!room || !isOmnichannelRoom(room)) {
33+
if (!room) {
3434
throw new Error('error-invalid-room');
3535
}
3636

37-
if (!room.open) {
38-
const { deletedCount } = await Subscriptions.removeByRoomId(roomId, {
39-
async onTrash(doc) {
40-
void notifyOnSubscriptionChanged(doc, 'removed');
41-
},
42-
});
43-
if (deletedCount) {
44-
return;
45-
}
46-
throw new Error('error-room-already-closed');
47-
}
48-
4937
const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, user._id, { projection: { _id: 1 } });
5038
if (!subscription && !(await hasPermissionAsync(user._id, 'close-others-livechat-room'))) {
5139
throw new Error('error-not-authorized');
@@ -76,7 +64,21 @@ export const closeLivechatRoom = async (
7664
}),
7765
};
7866

79-
await Livechat.closeRoom({
67+
if (forceClose) {
68+
return Livechat.closeRoom({
69+
room,
70+
user,
71+
options,
72+
comment,
73+
forceClose,
74+
});
75+
}
76+
77+
if (!room.open) {
78+
throw new Error('error-room-already-closed');
79+
}
80+
81+
return Livechat.closeRoom({
8082
room,
8183
user,
8284
options,

apps/meteor/app/livechat/server/api/v1/room.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { settings as rcSettings } from '../../../../settings/server';
2525
import { normalizeTransferredByData } from '../../lib/Helper';
2626
import { Livechat as LivechatTyped } from '../../lib/LivechatTyped';
2727
import type { CloseRoomParams } from '../../lib/localTypes';
28+
import { livechatLogger } from '../../lib/logger';
2829
import { findGuest, findRoom, settings, findAgent, onCheckRoomParams } from '../lib/livechat';
2930

3031
const isAgentWithInfo = (agentObj: ILivechatAgent | { hiddenInfo: boolean }): agentObj is ILivechatAgent => !('hiddenInfo' in agentObj);
@@ -195,9 +196,22 @@ API.v1.addRoute(
195196
},
196197
{
197198
async post() {
198-
const { rid, comment, tags, generateTranscriptPdf, transcriptEmail } = this.bodyParams;
199+
const { rid, comment, tags, generateTranscriptPdf, transcriptEmail, forceClose } = this.bodyParams;
199200

200-
await closeLivechatRoom(this.user, rid, { comment, tags, generateTranscriptPdf, transcriptEmail });
201+
const allowForceClose = rcSettings.get<boolean>('Omnichannel_allow_force_close_conversations');
202+
const isForceClosing = allowForceClose && forceClose;
203+
204+
if (isForceClosing) {
205+
livechatLogger.warn({ msg: 'Force closing a conversation', user: this.userId, room: rid });
206+
}
207+
208+
await closeLivechatRoom(this.user, rid, {
209+
comment,
210+
tags,
211+
generateTranscriptPdf,
212+
transcriptEmail,
213+
forceClose: isForceClosing,
214+
});
201215

202216
return API.v1.success();
203217
},

apps/meteor/app/livechat/server/lib/LivechatTyped.ts

+15-13
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,10 @@ class LivechatClass {
240240
session: ClientSession,
241241
): Promise<{ room: IOmnichannelRoom; closedBy: ChatCloser; removedInquiry: ILivechatInquiryRecord | null }> {
242242
const { comment } = params;
243-
const { room } = params;
243+
const { room, forceClose } = params;
244244

245-
this.logger.debug(`Attempting to close room ${room._id}`);
246-
if (!room || !isOmnichannelRoom(room) || !room.open) {
245+
this.logger.debug({ msg: `Attempting to close room`, roomId: room._id, forceClose });
246+
if (!room || !isOmnichannelRoom(room) || (!forceClose && !room.open)) {
247247
this.logger.debug(`Room ${room._id} is not open`);
248248
throw new Error('error-room-closed');
249249
}
@@ -292,25 +292,27 @@ class LivechatClass {
292292

293293
const inquiry = await LivechatInquiry.findOneByRoomId(rid, { session });
294294
const removedInquiry = await LivechatInquiry.removeByRoomId(rid, { session });
295-
if (removedInquiry && removedInquiry.deletedCount !== 1) {
295+
if (!params.forceClose && removedInquiry && removedInquiry.deletedCount !== 1) {
296296
throw new Error('Error removing inquiry');
297297
}
298298

299299
const updatedRoom = await LivechatRooms.closeRoomById(rid, closeData, { session });
300-
if (!updatedRoom || updatedRoom.modifiedCount !== 1) {
300+
if (!params.forceClose && (!updatedRoom || updatedRoom.modifiedCount !== 1)) {
301301
throw new Error('Error closing room');
302302
}
303303

304304
const subs = await Subscriptions.countByRoomId(rid, { session });
305-
const removedSubs = await Subscriptions.removeByRoomId(rid, {
306-
async onTrash(doc) {
307-
void notifyOnSubscriptionChanged(doc, 'removed');
308-
},
309-
session,
310-
});
305+
if (subs) {
306+
const removedSubs = await Subscriptions.removeByRoomId(rid, {
307+
async onTrash(doc) {
308+
void notifyOnSubscriptionChanged(doc, 'removed');
309+
},
310+
session,
311+
});
311312

312-
if (removedSubs.deletedCount !== subs) {
313-
throw new Error('Error removing subscriptions');
313+
if (!params.forceClose && removedSubs.deletedCount !== subs) {
314+
throw new Error('Error removing subscriptions');
315+
}
314316
}
315317

316318
this.logger.debug(`DB updated for room ${room._id}`);

apps/meteor/app/livechat/server/lib/localTypes.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { IOmnichannelRoom, IUser, ILivechatVisitor, IMessage, MessageAttach
33
type GenericCloseRoomParams = {
44
room: IOmnichannelRoom;
55
comment?: string;
6+
forceClose?: boolean;
67
options?: {
78
clientAction?: boolean;
89
tags?: string[];

apps/meteor/app/meteor-accounts-saml/server/definition/IServiceProviderOptions.ts

-4
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,4 @@ export interface IServiceProviderOptions {
2020
metadataCertificateTemplate: string;
2121
metadataTemplate: string;
2222
callbackUrl: string;
23-
24-
// The id and redirectUrl attributes are filled midway through some operations
25-
id?: string;
26-
redirectUrl?: string;
2723
}

apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts

+3-13
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class SAML {
5454
case 'sloRedirect':
5555
return this.processSLORedirectAction(req, res);
5656
case 'authorize':
57-
return this.processAuthorizeAction(req, res, service, samlObject);
57+
return this.processAuthorizeAction(res, service, samlObject);
5858
case 'validate':
5959
return this.processValidateAction(req, res, service, samlObject);
6060
default:
@@ -373,25 +373,15 @@ export class SAML {
373373
}
374374

375375
private static async processAuthorizeAction(
376-
req: IIncomingMessage,
377376
res: ServerResponse,
378377
service: IServiceProviderOptions,
379378
samlObject: ISAMLAction,
380379
): Promise<void> {
381-
service.id = samlObject.credentialToken;
382-
383-
// Allow redirecting to internal domains when login process is complete
384-
const { referer } = req.headers;
385-
const siteUrl = settings.get<string>('Site_Url');
386-
if (typeof referer === 'string' && referer.startsWith(siteUrl)) {
387-
service.redirectUrl = referer;
388-
}
389-
390380
const serviceProvider = new SAMLServiceProvider(service);
391381
let url: string | undefined;
392382

393383
try {
394-
url = await serviceProvider.getAuthorizeUrl();
384+
url = await serviceProvider.getAuthorizeUrl(samlObject.credentialToken);
395385
} catch (err: any) {
396386
SAMLUtils.error('Unable to generate authorize url');
397387
SAMLUtils.error(err);
@@ -433,7 +423,7 @@ export class SAML {
433423
};
434424

435425
await this.storeCredential(credentialToken, loginResult);
436-
const url = Meteor.absoluteUrl(SAMLUtils.getValidationActionRedirectPath(credentialToken, service.redirectUrl));
426+
const url = Meteor.absoluteUrl(SAMLUtils.getValidationActionRedirectPath(credentialToken));
437427
res.writeHead(302, {
438428
Location: url,
439429
});

apps/meteor/app/meteor-accounts-saml/server/lib/ServiceProvider.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ export class SAMLServiceProvider {
3535
return signer.sign(this.serviceProviderOptions.privateKey, 'base64');
3636
}
3737

38-
public generateAuthorizeRequest(): string {
39-
const identifiedRequest = AuthorizeRequest.generate(this.serviceProviderOptions);
38+
public generateAuthorizeRequest(credentialToken: string): string {
39+
const identifiedRequest = AuthorizeRequest.generate(this.serviceProviderOptions, credentialToken);
4040
return identifiedRequest.request;
4141
}
4242

@@ -151,8 +151,8 @@ export class SAMLServiceProvider {
151151
}
152152
}
153153

154-
public async getAuthorizeUrl(): Promise<string | undefined> {
155-
const request = this.generateAuthorizeRequest();
154+
public async getAuthorizeUrl(credentialToken: string): Promise<string | undefined> {
155+
const request = this.generateAuthorizeRequest(credentialToken);
156156
SAMLUtils.log('-----REQUEST------');
157157
SAMLUtils.log(request);
158158

apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,9 @@ export class SAMLUtils {
131131
return newTemplate;
132132
}
133133

134-
public static getValidationActionRedirectPath(credentialToken: string, redirectUrl?: string): string {
135-
const redirectUrlParam = redirectUrl ? `&redirectUrl=${encodeURIComponent(redirectUrl)}` : '';
134+
public static getValidationActionRedirectPath(credentialToken: string): string {
136135
// the saml_idp_credentialToken param is needed by the mobile app
137-
return `saml/${credentialToken}?saml_idp_credentialToken=${credentialToken}${redirectUrlParam}`;
136+
return `saml/${credentialToken}?saml_idp_credentialToken=${credentialToken}`;
138137
}
139138

140139
public static log(obj: any, ...args: Array<any>): void {

apps/meteor/app/meteor-accounts-saml/server/lib/generators/AuthorizeRequest.ts

+7-9
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import {
1414
An Authorize Request is used to show the Identity Provider login form when the user clicks on the Rocket.Chat SAML login button
1515
*/
1616
export class AuthorizeRequest {
17-
public static generate(serviceProviderOptions: IServiceProviderOptions): ISAMLRequest {
18-
const data = this.getDataForNewRequest(serviceProviderOptions);
17+
public static generate(serviceProviderOptions: IServiceProviderOptions, credentialToken: string): ISAMLRequest {
18+
const data = this.getDataForNewRequest(serviceProviderOptions, credentialToken);
1919
const request = SAMLUtils.fillTemplateData(this.authorizeRequestTemplate(serviceProviderOptions), data);
2020

2121
return {
@@ -53,15 +53,13 @@ export class AuthorizeRequest {
5353
return serviceProviderOptions.authnContextTemplate || defaultAuthnContextTemplate;
5454
}
5555

56-
private static getDataForNewRequest(serviceProviderOptions: IServiceProviderOptions): IAuthorizeRequestVariables {
57-
let id = `_${SAMLUtils.generateUniqueID()}`;
56+
private static getDataForNewRequest(
57+
serviceProviderOptions: IServiceProviderOptions,
58+
credentialToken?: string,
59+
): IAuthorizeRequestVariables {
60+
const id = credentialToken || `_${SAMLUtils.generateUniqueID()}`;
5861
const instant = SAMLUtils.generateInstant();
5962

60-
// Post-auth destination
61-
if (serviceProviderOptions.id) {
62-
id = serviceProviderOptions.id;
63-
}
64-
6563
return {
6664
newId: id,
6765
instant,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { RoomType } from '@rocket.chat/core-typings';
2+
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
3+
import type { TranslationKey } from '@rocket.chat/ui-contexts';
4+
import { useEndpoint, useRouter, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
5+
import { useTranslation } from 'react-i18next';
6+
7+
import { LegacyRoomManager } from '../../../app/ui-utils/client';
8+
import { UiTextContext } from '../../../definition/IRoomTypeConfig';
9+
import WarningModal from '../../components/WarningModal';
10+
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
11+
12+
const leaveEndpoints = {
13+
p: '/v1/groups.leave',
14+
c: '/v1/channels.leave',
15+
d: '/v1/im.leave',
16+
v: '/v1/channels.leave',
17+
l: '/v1/groups.leave',
18+
} as const;
19+
20+
type LeaveRoomProps = {
21+
rid: string;
22+
type: RoomType;
23+
name: string;
24+
roomOpen?: boolean;
25+
};
26+
27+
// TODO: this menu action should consider team leaving
28+
export const useLeaveRoomAction = ({ rid, type, name, roomOpen }: LeaveRoomProps) => {
29+
const { t } = useTranslation();
30+
const setModal = useSetModal();
31+
const dispatchToastMessage = useToastMessageDispatch();
32+
const router = useRouter();
33+
34+
const leaveRoom = useEndpoint('POST', leaveEndpoints[type]);
35+
36+
const handleLeave = useEffectEvent(() => {
37+
const leave = async (): Promise<void> => {
38+
try {
39+
await leaveRoom({ roomId: rid });
40+
if (roomOpen) {
41+
router.navigate('/home');
42+
}
43+
LegacyRoomManager.close(rid);
44+
} catch (error) {
45+
dispatchToastMessage({ type: 'error', message: error });
46+
} finally {
47+
setModal(null);
48+
}
49+
};
50+
51+
const warnText = roomCoordinator.getRoomDirectives(type).getUiText(UiTextContext.LEAVE_WARNING);
52+
53+
setModal(
54+
<WarningModal
55+
text={t(warnText as TranslationKey, name)}
56+
confirmText={t('Leave_room')}
57+
close={() => setModal(null)}
58+
cancelText={t('Cancel')}
59+
confirm={leave}
60+
/>,
61+
);
62+
});
63+
64+
return handleLeave;
65+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { IRoom } from '@rocket.chat/core-typings';
2+
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
3+
import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
4+
5+
export const useToggleFavoriteAction = ({ rid, isFavorite }: { rid: IRoom['_id']; isFavorite: boolean }) => {
6+
const toggleFavorite = useEndpoint('POST', '/v1/rooms.favorite');
7+
const dispatchToastMessage = useToastMessageDispatch();
8+
9+
const handleToggleFavorite = useEffectEvent(async () => {
10+
try {
11+
await toggleFavorite({ roomId: rid, favorite: !isFavorite });
12+
} catch (error) {
13+
dispatchToastMessage({ type: 'error', message: error });
14+
}
15+
});
16+
17+
return handleToggleFavorite;
18+
};

0 commit comments

Comments
 (0)