Skip to content

Commit

Permalink
Merge branch 'develop' into feat/add-endpoints-groups.membersOrderedB…
Browse files Browse the repository at this point in the history
…yRole-channels.membersOrderedByRole
  • Loading branch information
kodiakhq[bot] authored Jan 17, 2025
2 parents 7ee0f4b + 3c237b2 commit dbe0674
Show file tree
Hide file tree
Showing 66 changed files with 2,047 additions and 1,290 deletions.
7 changes: 7 additions & 0 deletions .changeset/eight-humans-sip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/i18n": minor
"@rocket.chat/rest-typings": minor
---

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.
5 changes: 5 additions & 0 deletions .changeset/itchy-pumas-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Fixes SAML login redirecting to wrong room when using an invite link.
6 changes: 6 additions & 0 deletions .changeset/metal-pets-promise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/i18n": minor
---

Fixes an issue where users without the "Preview public channel" permission would receive new messages sent to the channel
574 changes: 287 additions & 287 deletions .yarn/releases/yarn-4.5.3.cjs → .yarn/releases/yarn-4.6.0.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ plugins:
- path: .yarn/plugins/@yarnpkg/plugin-engines.cjs
spec: "https://raw.githubusercontent.com/devoto13/yarn-plugin-engines/main/bundles/%40yarnpkg/plugin-engines.js"

yarnPath: .yarn/releases/yarn-4.5.3.cjs
yarnPath: .yarn/releases/yarn-4.6.0.cjs
34 changes: 18 additions & 16 deletions apps/meteor/app/lib/server/functions/closeLivechatRoom.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import type { IUser, IRoom, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms, Subscriptions } from '@rocket.chat/models';

import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import { Livechat } from '../../../livechat/server/lib/LivechatTyped';
import type { CloseRoomParams } from '../../../livechat/server/lib/localTypes';
import { notifyOnSubscriptionChanged } from '../lib/notifyListener';

export const closeLivechatRoom = async (
user: IUser,
Expand All @@ -15,6 +13,7 @@ export const closeLivechatRoom = async (
tags,
generateTranscriptPdf,
transcriptEmail,
forceClose = false,
}: {
comment?: string;
tags?: string[];
Expand All @@ -27,25 +26,14 @@ export const closeLivechatRoom = async (
sendToVisitor: true;
requestData: Pick<NonNullable<IOmnichannelRoom['transcriptRequest']>, 'email' | 'subject'>;
};
forceClose?: boolean;
},
): Promise<void> => {
const room = await LivechatRooms.findOneById(roomId);
if (!room || !isOmnichannelRoom(room)) {
if (!room) {
throw new Error('error-invalid-room');
}

if (!room.open) {
const { deletedCount } = await Subscriptions.removeByRoomId(roomId, {
async onTrash(doc) {
void notifyOnSubscriptionChanged(doc, 'removed');
},
});
if (deletedCount) {
return;
}
throw new Error('error-room-already-closed');
}

const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, user._id, { projection: { _id: 1 } });
if (!subscription && !(await hasPermissionAsync(user._id, 'close-others-livechat-room'))) {
throw new Error('error-not-authorized');
Expand Down Expand Up @@ -76,7 +64,21 @@ export const closeLivechatRoom = async (
}),
};

await Livechat.closeRoom({
if (forceClose) {
return Livechat.closeRoom({
room,
user,
options,
comment,
forceClose,
});
}

if (!room.open) {
throw new Error('error-room-already-closed');
}

return Livechat.closeRoom({
room,
user,
options,
Expand Down
18 changes: 16 additions & 2 deletions apps/meteor/app/livechat/server/api/v1/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { settings as rcSettings } from '../../../../settings/server';
import { normalizeTransferredByData } from '../../lib/Helper';
import { Livechat as LivechatTyped } from '../../lib/LivechatTyped';
import type { CloseRoomParams } from '../../lib/localTypes';
import { livechatLogger } from '../../lib/logger';
import { findGuest, findRoom, settings, findAgent, onCheckRoomParams } from '../lib/livechat';

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

await closeLivechatRoom(this.user, rid, { comment, tags, generateTranscriptPdf, transcriptEmail });
const allowForceClose = rcSettings.get<boolean>('Omnichannel_allow_force_close_conversations');
const isForceClosing = allowForceClose && forceClose;

if (isForceClosing) {
livechatLogger.warn({ msg: 'Force closing a conversation', user: this.userId, room: rid });
}

await closeLivechatRoom(this.user, rid, {
comment,
tags,
generateTranscriptPdf,
transcriptEmail,
forceClose: isForceClosing,
});

return API.v1.success();
},
Expand Down
28 changes: 15 additions & 13 deletions apps/meteor/app/livechat/server/lib/LivechatTyped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,10 +240,10 @@ class LivechatClass {
session: ClientSession,
): Promise<{ room: IOmnichannelRoom; closedBy: ChatCloser; removedInquiry: ILivechatInquiryRecord | null }> {
const { comment } = params;
const { room } = params;
const { room, forceClose } = params;

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

const inquiry = await LivechatInquiry.findOneByRoomId(rid, { session });
const removedInquiry = await LivechatInquiry.removeByRoomId(rid, { session });
if (removedInquiry && removedInquiry.deletedCount !== 1) {
if (!params.forceClose && removedInquiry && removedInquiry.deletedCount !== 1) {
throw new Error('Error removing inquiry');
}

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

const subs = await Subscriptions.countByRoomId(rid, { session });
const removedSubs = await Subscriptions.removeByRoomId(rid, {
async onTrash(doc) {
void notifyOnSubscriptionChanged(doc, 'removed');
},
session,
});
if (subs) {
const removedSubs = await Subscriptions.removeByRoomId(rid, {
async onTrash(doc) {
void notifyOnSubscriptionChanged(doc, 'removed');
},
session,
});

if (removedSubs.deletedCount !== subs) {
throw new Error('Error removing subscriptions');
if (!params.forceClose && removedSubs.deletedCount !== subs) {
throw new Error('Error removing subscriptions');
}
}

this.logger.debug(`DB updated for room ${room._id}`);
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/app/livechat/server/lib/localTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { IOmnichannelRoom, IUser, ILivechatVisitor, IMessage, MessageAttach
type GenericCloseRoomParams = {
room: IOmnichannelRoom;
comment?: string;
forceClose?: boolean;
options?: {
clientAction?: boolean;
tags?: string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,4 @@ export interface IServiceProviderOptions {
metadataCertificateTemplate: string;
metadataTemplate: string;
callbackUrl: string;

// The id and redirectUrl attributes are filled midway through some operations
id?: string;
redirectUrl?: string;
}
16 changes: 3 additions & 13 deletions apps/meteor/app/meteor-accounts-saml/server/lib/SAML.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class SAML {
case 'sloRedirect':
return this.processSLORedirectAction(req, res);
case 'authorize':
return this.processAuthorizeAction(req, res, service, samlObject);
return this.processAuthorizeAction(res, service, samlObject);
case 'validate':
return this.processValidateAction(req, res, service, samlObject);
default:
Expand Down Expand Up @@ -373,25 +373,15 @@ export class SAML {
}

private static async processAuthorizeAction(
req: IIncomingMessage,
res: ServerResponse,
service: IServiceProviderOptions,
samlObject: ISAMLAction,
): Promise<void> {
service.id = samlObject.credentialToken;

// Allow redirecting to internal domains when login process is complete
const { referer } = req.headers;
const siteUrl = settings.get<string>('Site_Url');
if (typeof referer === 'string' && referer.startsWith(siteUrl)) {
service.redirectUrl = referer;
}

const serviceProvider = new SAMLServiceProvider(service);
let url: string | undefined;

try {
url = await serviceProvider.getAuthorizeUrl();
url = await serviceProvider.getAuthorizeUrl(samlObject.credentialToken);
} catch (err: any) {
SAMLUtils.error('Unable to generate authorize url');
SAMLUtils.error(err);
Expand Down Expand Up @@ -433,7 +423,7 @@ export class SAML {
};

await this.storeCredential(credentialToken, loginResult);
const url = Meteor.absoluteUrl(SAMLUtils.getValidationActionRedirectPath(credentialToken, service.redirectUrl));
const url = Meteor.absoluteUrl(SAMLUtils.getValidationActionRedirectPath(credentialToken));
res.writeHead(302, {
Location: url,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ export class SAMLServiceProvider {
return signer.sign(this.serviceProviderOptions.privateKey, 'base64');
}

public generateAuthorizeRequest(): string {
const identifiedRequest = AuthorizeRequest.generate(this.serviceProviderOptions);
public generateAuthorizeRequest(credentialToken: string): string {
const identifiedRequest = AuthorizeRequest.generate(this.serviceProviderOptions, credentialToken);
return identifiedRequest.request;
}

Expand Down Expand Up @@ -151,8 +151,8 @@ export class SAMLServiceProvider {
}
}

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

Expand Down
5 changes: 2 additions & 3 deletions apps/meteor/app/meteor-accounts-saml/server/lib/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,9 @@ export class SAMLUtils {
return newTemplate;
}

public static getValidationActionRedirectPath(credentialToken: string, redirectUrl?: string): string {
const redirectUrlParam = redirectUrl ? `&redirectUrl=${encodeURIComponent(redirectUrl)}` : '';
public static getValidationActionRedirectPath(credentialToken: string): string {
// the saml_idp_credentialToken param is needed by the mobile app
return `saml/${credentialToken}?saml_idp_credentialToken=${credentialToken}${redirectUrlParam}`;
return `saml/${credentialToken}?saml_idp_credentialToken=${credentialToken}`;
}

public static log(obj: any, ...args: Array<any>): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import {
An Authorize Request is used to show the Identity Provider login form when the user clicks on the Rocket.Chat SAML login button
*/
export class AuthorizeRequest {
public static generate(serviceProviderOptions: IServiceProviderOptions): ISAMLRequest {
const data = this.getDataForNewRequest(serviceProviderOptions);
public static generate(serviceProviderOptions: IServiceProviderOptions, credentialToken: string): ISAMLRequest {
const data = this.getDataForNewRequest(serviceProviderOptions, credentialToken);
const request = SAMLUtils.fillTemplateData(this.authorizeRequestTemplate(serviceProviderOptions), data);

return {
Expand Down Expand Up @@ -53,15 +53,13 @@ export class AuthorizeRequest {
return serviceProviderOptions.authnContextTemplate || defaultAuthnContextTemplate;
}

private static getDataForNewRequest(serviceProviderOptions: IServiceProviderOptions): IAuthorizeRequestVariables {
let id = `_${SAMLUtils.generateUniqueID()}`;
private static getDataForNewRequest(
serviceProviderOptions: IServiceProviderOptions,
credentialToken?: string,
): IAuthorizeRequestVariables {
const id = credentialToken || `_${SAMLUtils.generateUniqueID()}`;
const instant = SAMLUtils.generateInstant();

// Post-auth destination
if (serviceProviderOptions.id) {
id = serviceProviderOptions.id;
}

return {
newId: id,
instant,
Expand Down
65 changes: 65 additions & 0 deletions apps/meteor/client/hooks/menuActions/useLeaveRoom.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { RoomType } from '@rocket.chat/core-typings';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import { useEndpoint, useRouter, useSetModal, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { useTranslation } from 'react-i18next';

import { LegacyRoomManager } from '../../../app/ui-utils/client';
import { UiTextContext } from '../../../definition/IRoomTypeConfig';
import WarningModal from '../../components/WarningModal';
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';

const leaveEndpoints = {
p: '/v1/groups.leave',
c: '/v1/channels.leave',
d: '/v1/im.leave',
v: '/v1/channels.leave',
l: '/v1/groups.leave',
} as const;

type LeaveRoomProps = {
rid: string;
type: RoomType;
name: string;
roomOpen?: boolean;
};

// TODO: this menu action should consider team leaving
export const useLeaveRoomAction = ({ rid, type, name, roomOpen }: LeaveRoomProps) => {
const { t } = useTranslation();
const setModal = useSetModal();
const dispatchToastMessage = useToastMessageDispatch();
const router = useRouter();

const leaveRoom = useEndpoint('POST', leaveEndpoints[type]);

const handleLeave = useEffectEvent(() => {
const leave = async (): Promise<void> => {
try {
await leaveRoom({ roomId: rid });
if (roomOpen) {
router.navigate('/home');
}
LegacyRoomManager.close(rid);
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
setModal(null);
}
};

const warnText = roomCoordinator.getRoomDirectives(type).getUiText(UiTextContext.LEAVE_WARNING);

setModal(
<WarningModal
text={t(warnText as TranslationKey, name)}
confirmText={t('Leave_room')}
close={() => setModal(null)}
cancelText={t('Cancel')}
confirm={leave}
/>,
);
});

return handleLeave;
};
18 changes: 18 additions & 0 deletions apps/meteor/client/hooks/menuActions/useToggleFavoriteAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts';

export const useToggleFavoriteAction = ({ rid, isFavorite }: { rid: IRoom['_id']; isFavorite: boolean }) => {
const toggleFavorite = useEndpoint('POST', '/v1/rooms.favorite');
const dispatchToastMessage = useToastMessageDispatch();

const handleToggleFavorite = useEffectEvent(async () => {
try {
await toggleFavorite({ roomId: rid, favorite: !isFavorite });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
});

return handleToggleFavorite;
};
Loading

0 comments on commit dbe0674

Please sign in to comment.