snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
37
apps/meteor/server/lib/utils/functions/getBaseUserFields.ts
Normal file
37
apps/meteor/server/lib/utils/functions/getBaseUserFields.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
type UserFields = {
|
||||
[k: string]: number;
|
||||
};
|
||||
|
||||
export const getBaseUserFields = (allowServiceKeys = false): UserFields => ({
|
||||
'name': 1,
|
||||
'username': 1,
|
||||
'nickname': 1,
|
||||
'emails': 1,
|
||||
'status': 1,
|
||||
'statusDefault': 1,
|
||||
'statusText': 1,
|
||||
'statusSource': 1,
|
||||
'statusExpiresAt': 1,
|
||||
'statusConnection': 1,
|
||||
'bio': 1,
|
||||
'avatarOrigin': 1,
|
||||
'utcOffset': 1,
|
||||
'language': 1,
|
||||
'settings': 1,
|
||||
'enableAutoAway': 1,
|
||||
'idleTimeLimit': 1,
|
||||
'roles': 1,
|
||||
'active': 1,
|
||||
'defaultRoom': 1,
|
||||
'customFields': 1,
|
||||
'requirePasswordChange': 1,
|
||||
'requirePasswordChangeReason': 1,
|
||||
'statusLivechat': 1,
|
||||
'banners': 1,
|
||||
'oauth.authorizedClients': 1,
|
||||
'_updatedAt': 1,
|
||||
'avatarETag': 1,
|
||||
'openBusinessHours': 1,
|
||||
'abacAttributes': 1,
|
||||
...(allowServiceKeys && { 'services.totp.enabled': 1, 'services.email2fa.enabled': 1 }),
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { getBaseUserFields } from './getBaseUserFields';
|
||||
|
||||
type UserFields = {
|
||||
[k: string]: number;
|
||||
};
|
||||
|
||||
export const getDefaultUserFields = (): UserFields => ({
|
||||
...getBaseUserFields(true),
|
||||
'services.github': 1,
|
||||
'services.gitlab': 1,
|
||||
'services.password.bcrypt': 1,
|
||||
});
|
||||
61
apps/meteor/server/lib/utils/functions/getMongoInfo.ts
Normal file
61
apps/meteor/server/lib/utils/functions/getMongoInfo.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { MongoInternals, type MongoConnection } from 'meteor/mongo';
|
||||
|
||||
function getOplogInfo(): { mongo: MongoConnection } {
|
||||
const { mongo } = MongoInternals.defaultRemoteCollectionDriver();
|
||||
|
||||
return { mongo };
|
||||
}
|
||||
|
||||
async function fallbackMongoInfo(): Promise<{
|
||||
mongoVersion: string;
|
||||
mongoStorageEngine?: string;
|
||||
mongo: MongoConnection;
|
||||
}> {
|
||||
let mongoVersion;
|
||||
let mongoStorageEngine;
|
||||
|
||||
const { mongo } = getOplogInfo();
|
||||
|
||||
try {
|
||||
const { version } = await mongo.db.command({ buildinfo: 1 });
|
||||
mongoVersion = version;
|
||||
mongoStorageEngine = 'unknown';
|
||||
} catch (e) {
|
||||
console.error('=== Error getting MongoDB info ===');
|
||||
console.error(e?.toString());
|
||||
console.error('----------------------------------');
|
||||
console.error("Without mongodb version we can't ensure you are running a compatible version.");
|
||||
console.error('If you are running your mongodb with auth enabled and an user different from admin');
|
||||
console.error('you may need to grant permissions for this user to check cluster data.');
|
||||
console.error('You can do it via mongo shell running the following command replacing');
|
||||
console.error("the string YOUR_USER by the correct user's name:");
|
||||
console.error('');
|
||||
console.error(' db.runCommand({ grantRolesToUser: "YOUR_USER" , roles: [{role: "clusterMonitor", db: "admin"}]})');
|
||||
console.error('');
|
||||
console.error('==================================');
|
||||
}
|
||||
|
||||
return { mongoVersion, mongoStorageEngine, mongo };
|
||||
}
|
||||
|
||||
export async function getMongoInfo(): Promise<{
|
||||
mongoVersion: string;
|
||||
mongoStorageEngine?: string;
|
||||
mongo: MongoConnection;
|
||||
}> {
|
||||
let mongoVersion;
|
||||
let mongoStorageEngine;
|
||||
|
||||
const { mongo } = getOplogInfo();
|
||||
|
||||
try {
|
||||
const { version, storageEngine } = await mongo.db.command({ serverStatus: 1 });
|
||||
|
||||
mongoVersion = version;
|
||||
mongoStorageEngine = storageEngine.name;
|
||||
} catch (e) {
|
||||
return fallbackMongoInfo();
|
||||
}
|
||||
|
||||
return { mongoVersion, mongoStorageEngine, mongo };
|
||||
}
|
||||
31
apps/meteor/server/lib/utils/functions/isDocker.ts
Normal file
31
apps/meteor/server/lib/utils/functions/isDocker.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import fs from 'node:fs';
|
||||
|
||||
function hasDockerEnv(): boolean {
|
||||
try {
|
||||
fs.statSync('/.dockerenv');
|
||||
return true;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function hasDockerCGroup(): boolean {
|
||||
try {
|
||||
return fs.readFileSync('/proc/self/cgroup', 'utf8').indexOf('docker') !== -1;
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function check(): boolean {
|
||||
return hasDockerEnv() || hasDockerCGroup();
|
||||
}
|
||||
|
||||
let _isDocker: boolean;
|
||||
export const isDocker = function (): boolean {
|
||||
if (_isDocker === undefined) {
|
||||
_isDocker = check();
|
||||
}
|
||||
|
||||
return _isDocker;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { settings } from '../../../settings';
|
||||
|
||||
export const isSMTPConfigured = (): boolean => {
|
||||
const isMailURLSet = !(process.env.MAIL_URL === 'undefined' || process.env.MAIL_URL === undefined);
|
||||
return Boolean(settings.get('SMTP_Host')) || isMailURLSet;
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { IMessage } from '@rocket.chat/core-typings';
|
||||
import { Uploads } from '@rocket.chat/models';
|
||||
|
||||
import { FileUpload } from '../../media/file-upload';
|
||||
import { getURL } from '../getURL';
|
||||
|
||||
export const normalizeMessageFileUpload = async (message: Omit<IMessage, '_updatedAt'>): Promise<Omit<IMessage, '_updatedAt'>> => {
|
||||
if (message.file && !message.fileUpload) {
|
||||
const jwt = FileUpload.generateJWTToFileUrls({
|
||||
rid: message.rid,
|
||||
userId: message.u._id,
|
||||
fileId: message.file._id,
|
||||
});
|
||||
const file = await Uploads.findOne({ _id: message.file._id });
|
||||
if (!file) {
|
||||
return message;
|
||||
}
|
||||
message.fileUpload = {
|
||||
publicFilePath: file.name
|
||||
? getURL(`${FileUpload.getPath(`${file._id}/${encodeURI(file.name)}`).substring(1)}${jwt ? `?token=${jwt}` : ''}`, {
|
||||
cdn: false,
|
||||
full: true,
|
||||
})
|
||||
: '',
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
};
|
||||
}
|
||||
return message;
|
||||
};
|
||||
10
apps/meteor/server/lib/utils/getAvatarURL.ts
Normal file
10
apps/meteor/server/lib/utils/getAvatarURL.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { getURL } from './getURL';
|
||||
|
||||
export const getAvatarURL = ({ username, roomId, cache }: { username?: string; roomId?: string; cache?: string }): string | undefined => {
|
||||
if (username) {
|
||||
return getURL(`/avatar/${encodeURIComponent(username)}${cache ? `?etag=${cache}` : ''}`);
|
||||
}
|
||||
if (roomId) {
|
||||
return getURL(`/avatar/room/${encodeURIComponent(roomId)}${cache ? `?etag=${cache}` : ''}`);
|
||||
}
|
||||
};
|
||||
19
apps/meteor/server/lib/utils/getURL.ts
Normal file
19
apps/meteor/server/lib/utils/getURL.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { getURLWithoutSettings } from '../../../app/utils/lib/getURL';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
export const getURL = function (
|
||||
path: string,
|
||||
params: {
|
||||
cdn?: boolean;
|
||||
full?: boolean;
|
||||
cloud?: boolean;
|
||||
cloud_route?: string;
|
||||
cloud_params?: Record<string, string>;
|
||||
} = {},
|
||||
cloudDeepLinkUrl?: string,
|
||||
): string {
|
||||
const cdnPrefix = settings.get<string>('CDN_PREFIX') || '';
|
||||
const siteUrl = settings.get<string>('Site_Url') || '';
|
||||
|
||||
return getURLWithoutSettings(path, params, cdnPrefix, siteUrl, cloudDeepLinkUrl);
|
||||
};
|
||||
9
apps/meteor/server/lib/utils/getUserAvatarURL.ts
Normal file
9
apps/meteor/server/lib/utils/getUserAvatarURL.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { getAvatarURL } from './getAvatarURL';
|
||||
|
||||
export const getUserAvatarURL = function (username: string, cache = ''): string | undefined {
|
||||
if (username == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
return getAvatarURL({ username, cache });
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
import { Users } from '@rocket.chat/models';
|
||||
|
||||
import { settings } from '../../settings';
|
||||
|
||||
export const getUserNotificationPreference = async (user: IUser | string, pref: string) => {
|
||||
if (typeof user === 'string') {
|
||||
const u = await Users.findOneById(user);
|
||||
if (!u) {
|
||||
return null;
|
||||
}
|
||||
user = u;
|
||||
}
|
||||
|
||||
let preferenceKey: 'desktopNotifications' | 'pushNotifications' | 'emailNotificationMode' | undefined;
|
||||
switch (pref) {
|
||||
case 'desktop':
|
||||
preferenceKey = 'desktopNotifications';
|
||||
break;
|
||||
case 'mobile':
|
||||
preferenceKey = 'pushNotifications';
|
||||
break;
|
||||
case 'email':
|
||||
preferenceKey = 'emailNotificationMode';
|
||||
break;
|
||||
}
|
||||
|
||||
if (!preferenceKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
user?.settings?.preferences &&
|
||||
typeof user.settings.preferences[preferenceKey] !== 'undefined' &&
|
||||
user.settings.preferences[preferenceKey] !== 'default'
|
||||
) {
|
||||
return {
|
||||
value: user.settings.preferences[preferenceKey],
|
||||
origin: 'user',
|
||||
};
|
||||
}
|
||||
const serverValue = settings.get(`Accounts_Default_User_Preferences_${preferenceKey}`);
|
||||
if (typeof serverValue !== 'undefined') {
|
||||
return {
|
||||
value: serverValue,
|
||||
origin: 'server',
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
299
apps/meteor/server/lib/utils/lib/JWTHelper.spec.ts
Normal file
299
apps/meteor/server/lib/utils/lib/JWTHelper.spec.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it, beforeEach } from 'mocha';
|
||||
import proxyquire from 'proxyquire';
|
||||
import sinon from 'sinon';
|
||||
|
||||
const jsrsasignStub = {
|
||||
KJUR: {
|
||||
jws: {
|
||||
IntDate: {
|
||||
get: sinon.stub(),
|
||||
},
|
||||
JWS: {
|
||||
sign: sinon.stub(),
|
||||
verify: sinon.stub(),
|
||||
parse: sinon.stub(),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const { generateJWT, validateAndDecodeJWT } = proxyquire.noCallThru().load('./JWTHelper', {
|
||||
jsrsasign: jsrsasignStub,
|
||||
});
|
||||
|
||||
describe('JWTHelper', () => {
|
||||
beforeEach(() => {
|
||||
jsrsasignStub.KJUR.jws.IntDate.get.reset();
|
||||
jsrsasignStub.KJUR.jws.JWS.sign.reset();
|
||||
jsrsasignStub.KJUR.jws.JWS.verify.reset();
|
||||
jsrsasignStub.KJUR.jws.JWS.parse.reset();
|
||||
});
|
||||
|
||||
describe('generateJWT', () => {
|
||||
it('should generate a JWT with the correct payload structure', () => {
|
||||
const now = 1609459200;
|
||||
const expiryDuration = 3600; // 1 hour in seconds
|
||||
const expiryTime = now + expiryDuration;
|
||||
|
||||
jsrsasignStub.KJUR.jws.IntDate.get.callsFake((timeStr) => {
|
||||
if (timeStr === 'now') return now;
|
||||
if (timeStr === 'now + 1hour') return expiryTime;
|
||||
return now;
|
||||
});
|
||||
jsrsasignStub.KJUR.jws.JWS.sign.returns('mocked.jwt.token');
|
||||
|
||||
const payload = { userId: 'user-123', roomId: 'room-456' };
|
||||
const secret = 'test-secret';
|
||||
|
||||
const result = generateJWT(payload, secret);
|
||||
|
||||
expect(result).to.equal('mocked.jwt.token');
|
||||
expect(jsrsasignStub.KJUR.jws.JWS.sign.calledOnce).to.be.true;
|
||||
|
||||
const signArgs = jsrsasignStub.KJUR.jws.JWS.sign.firstCall.args;
|
||||
expect(signArgs[0]).to.equal('HS256');
|
||||
expect(signArgs[1]).to.equal(JSON.stringify({ typ: 'JWT', alg: 'HS256' }));
|
||||
|
||||
const tokenPayload = JSON.parse(signArgs[2]);
|
||||
expect(tokenPayload.iat).to.equal(now);
|
||||
expect(tokenPayload.nbf).to.equal(now);
|
||||
expect(tokenPayload.exp).to.equal(expiryTime);
|
||||
expect(tokenPayload.aud).to.equal('RocketChat');
|
||||
expect(tokenPayload.context).to.deep.equal(payload);
|
||||
|
||||
expect(signArgs[3]).to.deep.equal({ rstr: secret });
|
||||
});
|
||||
|
||||
it('should include the provided payload in the context field', () => {
|
||||
jsrsasignStub.KJUR.jws.IntDate.get.returns(1609459200);
|
||||
jsrsasignStub.KJUR.jws.JWS.sign.returns('mocked.jwt.token');
|
||||
|
||||
const payload = { fileId: 'file-123', rid: 'room-123', userId: 'user-123' };
|
||||
const secret = 'secret-key';
|
||||
|
||||
generateJWT(payload, secret);
|
||||
|
||||
const signArgs = jsrsasignStub.KJUR.jws.JWS.sign.firstCall.args;
|
||||
const tokenPayload = JSON.parse(signArgs[2]);
|
||||
|
||||
expect(tokenPayload.context).to.deep.equal(payload);
|
||||
});
|
||||
|
||||
it('should generate a JWT with custom audience when provided in options', () => {
|
||||
jsrsasignStub.KJUR.jws.IntDate.get.returns(1609459200);
|
||||
jsrsasignStub.KJUR.jws.JWS.sign.returns('mocked.jwt.token');
|
||||
|
||||
const payload = { userId: 'user-123' };
|
||||
const secret = 'test-secret';
|
||||
const customAudience = 'CustomApp';
|
||||
|
||||
generateJWT(payload, secret, { aud: customAudience });
|
||||
|
||||
const signArgs = jsrsasignStub.KJUR.jws.JWS.sign.firstCall.args;
|
||||
const tokenPayload = JSON.parse(signArgs[2]);
|
||||
|
||||
expect(tokenPayload.aud).to.equal(customAudience);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateAndDecodeJWT', () => {
|
||||
it('should return null if jwt is empty', () => {
|
||||
const result = validateAndDecodeJWT('', 'secret');
|
||||
expect(result).to.be.null;
|
||||
});
|
||||
|
||||
it('should return null if jwt is null', () => {
|
||||
const result = validateAndDecodeJWT(null as any, 'secret');
|
||||
expect(result).to.be.null;
|
||||
});
|
||||
|
||||
it('should return null if secret is empty', () => {
|
||||
const result = validateAndDecodeJWT('some.jwt.token', '');
|
||||
expect(result).to.be.null;
|
||||
});
|
||||
|
||||
it('should return null if secret is null', () => {
|
||||
const result = validateAndDecodeJWT('some.jwt.token', null as any);
|
||||
expect(result).to.be.null;
|
||||
});
|
||||
|
||||
it('should return null if signature is invalid', () => {
|
||||
jsrsasignStub.KJUR.jws.JWS.verify.returns(false);
|
||||
|
||||
const result = validateAndDecodeJWT('invalid.jwt.token', 'secret');
|
||||
|
||||
expect(result).to.be.null;
|
||||
expect(jsrsasignStub.KJUR.jws.JWS.verify.calledOnce).to.be.true;
|
||||
expect(jsrsasignStub.KJUR.jws.JWS.verify.calledWith('invalid.jwt.token', 'secret', ['HS256'])).to.be.true;
|
||||
});
|
||||
|
||||
it('should return null if token is expired', () => {
|
||||
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
|
||||
jsrsasignStub.KJUR.jws.JWS.parse.returns({
|
||||
payloadObj: {
|
||||
exp: Math.floor(Date.now() / 1000) - 3600, // Expired 1 hour ago
|
||||
nbf: Math.floor(Date.now() / 1000) - 7200,
|
||||
aud: 'RocketChat',
|
||||
context: { userId: 'user-123' },
|
||||
},
|
||||
});
|
||||
|
||||
const result = validateAndDecodeJWT('expired.jwt.token', 'secret');
|
||||
|
||||
expect(result).to.be.null;
|
||||
});
|
||||
|
||||
it('should return null if nbf (not before) is in the future', () => {
|
||||
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
|
||||
jsrsasignStub.KJUR.jws.JWS.parse.returns({
|
||||
payloadObj: {
|
||||
exp: Math.floor(Date.now() / 1000) + 7200,
|
||||
nbf: Math.floor(Date.now() / 1000) + 3600, // Not valid until 1 hour from now
|
||||
aud: 'RocketChat',
|
||||
context: { userId: 'user-123' },
|
||||
},
|
||||
});
|
||||
|
||||
const result = validateAndDecodeJWT('future.jwt.token', 'secret');
|
||||
|
||||
expect(result).to.be.null;
|
||||
});
|
||||
|
||||
it('should return null if audience is not RocketChat', () => {
|
||||
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
|
||||
jsrsasignStub.KJUR.jws.JWS.parse.returns({
|
||||
payloadObj: {
|
||||
exp: Math.floor(Date.now() / 1000) + 3600,
|
||||
nbf: Math.floor(Date.now() / 1000) - 60,
|
||||
aud: 'WrongAudience',
|
||||
context: { userId: 'user-123' },
|
||||
},
|
||||
});
|
||||
|
||||
const result = validateAndDecodeJWT('wrong-audience.jwt.token', 'secret');
|
||||
|
||||
expect(result).to.be.null;
|
||||
});
|
||||
|
||||
it('should return the context payload for a valid token with correct signature, timestamps, and audience', () => {
|
||||
const contextPayload = { userId: 'user-123', fileId: 'file-456', rid: 'room-789' };
|
||||
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
|
||||
jsrsasignStub.KJUR.jws.JWS.parse.returns({
|
||||
payloadObj: {
|
||||
exp: Math.floor(Date.now() / 1000) + 3600, // Expires in 1 hour
|
||||
nbf: Math.floor(Date.now() / 1000) - 60, // Valid since 1 minute ago
|
||||
aud: 'RocketChat',
|
||||
context: contextPayload,
|
||||
},
|
||||
});
|
||||
|
||||
const result = validateAndDecodeJWT('valid.jwt.token', 'secret');
|
||||
|
||||
expect(result).to.deep.equal(contextPayload);
|
||||
expect(jsrsasignStub.KJUR.jws.JWS.verify.calledOnce).to.be.true;
|
||||
expect(jsrsasignStub.KJUR.jws.JWS.parse.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it('should return the context payload if exp and nbf are not provided', () => {
|
||||
const contextPayload = { userId: 'user-123' };
|
||||
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
|
||||
jsrsasignStub.KJUR.jws.JWS.parse.returns({
|
||||
payloadObj: {
|
||||
aud: 'RocketChat',
|
||||
context: contextPayload,
|
||||
},
|
||||
});
|
||||
|
||||
const result = validateAndDecodeJWT('valid.jwt.token', 'secret');
|
||||
|
||||
expect(result).to.deep.equal(contextPayload);
|
||||
});
|
||||
|
||||
it('should return null if token cannot be parsed', () => {
|
||||
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
|
||||
jsrsasignStub.KJUR.jws.JWS.parse.throws(new Error('Parse error'));
|
||||
|
||||
const result = validateAndDecodeJWT('malformed.jwt.token', 'secret');
|
||||
|
||||
expect(result).to.be.null;
|
||||
});
|
||||
|
||||
it('should return null if verification throws an error', () => {
|
||||
jsrsasignStub.KJUR.jws.JWS.verify.throws(new Error('Verification error'));
|
||||
|
||||
const result = validateAndDecodeJWT('error.jwt.token', 'secret');
|
||||
|
||||
expect(result).to.be.null;
|
||||
});
|
||||
|
||||
it('should validate and decode token with custom audience when provided in options', () => {
|
||||
const contextPayload = { userId: 'user-123' };
|
||||
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
|
||||
jsrsasignStub.KJUR.jws.JWS.parse.returns({
|
||||
payloadObj: {
|
||||
exp: Math.floor(Date.now() / 1000) + 3600,
|
||||
nbf: Math.floor(Date.now() / 1000) - 60,
|
||||
aud: 'CustomApp',
|
||||
context: contextPayload,
|
||||
},
|
||||
});
|
||||
|
||||
const result = validateAndDecodeJWT('valid.jwt.token', 'secret', { aud: 'CustomApp' });
|
||||
|
||||
expect(result).to.deep.equal(contextPayload);
|
||||
});
|
||||
|
||||
it('should return null if custom audience does not match token audience', () => {
|
||||
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
|
||||
jsrsasignStub.KJUR.jws.JWS.parse.returns({
|
||||
payloadObj: {
|
||||
exp: Math.floor(Date.now() / 1000) + 3600,
|
||||
nbf: Math.floor(Date.now() / 1000) - 60,
|
||||
aud: 'CustomApp',
|
||||
context: { userId: 'user-123' },
|
||||
},
|
||||
});
|
||||
|
||||
const result = validateAndDecodeJWT('valid.jwt.token', 'secret', { aud: 'DifferentApp' });
|
||||
|
||||
expect(result).to.be.null;
|
||||
});
|
||||
|
||||
it('should return null if payloadObj is null', () => {
|
||||
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
|
||||
jsrsasignStub.KJUR.jws.JWS.parse.returns({
|
||||
payloadObj: null,
|
||||
});
|
||||
|
||||
const result = validateAndDecodeJWT('jwt.token', 'secret');
|
||||
|
||||
expect(result).to.be.null;
|
||||
});
|
||||
|
||||
it('should return null if context is missing from payload', () => {
|
||||
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
|
||||
jsrsasignStub.KJUR.jws.JWS.parse.returns({
|
||||
payloadObj: {
|
||||
iat: 1609459200,
|
||||
nbf: 1609459200,
|
||||
exp: 1609462800,
|
||||
aud: 'RocketChat',
|
||||
},
|
||||
});
|
||||
|
||||
const result = validateAndDecodeJWT('jwt.token', 'secret');
|
||||
|
||||
expect(result).to.be.null;
|
||||
});
|
||||
|
||||
it('should return null if parse returns undefined', () => {
|
||||
jsrsasignStub.KJUR.jws.JWS.verify.returns(true);
|
||||
jsrsasignStub.KJUR.jws.JWS.parse.returns(undefined);
|
||||
|
||||
const result = validateAndDecodeJWT('jwt.token', 'secret');
|
||||
|
||||
expect(result).to.be.null;
|
||||
});
|
||||
});
|
||||
});
|
||||
56
apps/meteor/server/lib/utils/lib/JWTHelper.ts
Normal file
56
apps/meteor/server/lib/utils/lib/JWTHelper.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import jsr from 'jsrsasign';
|
||||
|
||||
const HEADER = {
|
||||
typ: 'JWT',
|
||||
alg: 'HS256',
|
||||
};
|
||||
|
||||
export const generateJWT = (payload: Record<string, any>, secret: string, options?: { aud?: string }): string => {
|
||||
const tokenPayload = {
|
||||
iat: jsr.KJUR.jws.IntDate.get('now'),
|
||||
nbf: jsr.KJUR.jws.IntDate.get('now'),
|
||||
exp: jsr.KJUR.jws.IntDate.get('now + 1hour'),
|
||||
aud: options?.aud || 'RocketChat',
|
||||
context: payload,
|
||||
};
|
||||
|
||||
const header = JSON.stringify(HEADER);
|
||||
|
||||
return jsr.KJUR.jws.JWS.sign(HEADER.alg, header, JSON.stringify(tokenPayload), { rstr: secret });
|
||||
};
|
||||
|
||||
export const validateAndDecodeJWT = (jwt: string, secret: string, options?: { aud?: string }): Record<string, any> | null => {
|
||||
if (!jwt || !secret) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const isSignatureValid = jsr.KJUR.jws.JWS.verify(jwt, secret, [HEADER.alg]);
|
||||
if (!isSignatureValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const decoded = jsr.KJUR.jws.JWS.parse(jwt);
|
||||
if (!decoded?.payloadObj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const payload = decoded.payloadObj as Record<string, any>;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (typeof payload.exp === 'number' && now >= payload.exp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof payload.nbf === 'number' && now < payload.nbf) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (payload.aud !== (options?.aud || 'RocketChat')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return payload.context || null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
3
apps/meteor/server/lib/utils/lib/getAvatarColor.ts
Normal file
3
apps/meteor/server/lib/utils/lib/getAvatarColor.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
const colors = ['#158D65', '#7F1B9F', '#B68D00', '#E26D0E', '#10529E', '#6C727A'];
|
||||
|
||||
export const getAvatarColor = (name: string): string => colors[name.length % colors.length];
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { AtLeast, ISubscription, IUser } from '@rocket.chat/core-typings';
|
||||
|
||||
/**
|
||||
* @type {(userPref: Pick<import('@rocket.chat/core-typings').IUser, 'settings'>) => {
|
||||
* desktopPrefOrigin: 'user';
|
||||
* mobilePrefOrigin: 'user';
|
||||
* emailPrefOrigin: 'user';
|
||||
* }}
|
||||
*/
|
||||
export const getDefaultSubscriptionPref = (userPref: AtLeast<IUser, 'settings'>) => {
|
||||
const subscription: Partial<ISubscription> = {};
|
||||
|
||||
const { desktopNotifications, pushNotifications, emailNotificationMode, highlights } = userPref.settings?.preferences || {};
|
||||
|
||||
if (Array.isArray(highlights) && highlights.length) {
|
||||
subscription.userHighlights = highlights;
|
||||
}
|
||||
|
||||
if (desktopNotifications && desktopNotifications !== 'default') {
|
||||
subscription.desktopNotifications = desktopNotifications;
|
||||
subscription.desktopPrefOrigin = 'user';
|
||||
}
|
||||
|
||||
if (pushNotifications && pushNotifications !== 'default') {
|
||||
subscription.mobilePushNotifications = pushNotifications;
|
||||
subscription.mobilePrefOrigin = 'user';
|
||||
}
|
||||
|
||||
if (emailNotificationMode && emailNotificationMode !== 'default') {
|
||||
subscription.emailNotifications = emailNotificationMode;
|
||||
subscription.emailPrefOrigin = 'user';
|
||||
}
|
||||
|
||||
return subscription;
|
||||
};
|
||||
32
apps/meteor/server/lib/utils/lib/getTimezone.ts
Normal file
32
apps/meteor/server/lib/utils/lib/getTimezone.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import { settings } from '../../../settings';
|
||||
|
||||
const padOffset = (offset: string | number): string => {
|
||||
const numberOffset = Number(offset);
|
||||
const absOffset = Math.abs(numberOffset);
|
||||
const isNegative = !(numberOffset === absOffset);
|
||||
|
||||
return `${isNegative ? '-' : '+'}${absOffset < 10 ? `0${absOffset}` : absOffset}:00`;
|
||||
};
|
||||
|
||||
const guessTimezoneFromOffset = (offset?: string | number): string => {
|
||||
if (!offset) {
|
||||
return moment.tz.guess();
|
||||
}
|
||||
|
||||
return moment.tz.names().find((tz) => padOffset(offset) === moment.tz(tz).format('Z').toString()) || moment.tz.guess();
|
||||
};
|
||||
|
||||
export const getTimezone = (user?: { utcOffset?: string | number } | null): string => {
|
||||
const timezone = settings.get('Default_Timezone_For_Reporting');
|
||||
|
||||
switch (timezone) {
|
||||
case 'custom':
|
||||
return settings.get<string>('Default_Custom_Timezone');
|
||||
case 'user':
|
||||
return guessTimezoneFromOffset(user?.utcOffset);
|
||||
default:
|
||||
return moment.tz.guess();
|
||||
}
|
||||
};
|
||||
32
apps/meteor/server/lib/utils/lib/getUserPreference.ts
Normal file
32
apps/meteor/server/lib/utils/lib/getUserPreference.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
import { Users } from '@rocket.chat/models';
|
||||
|
||||
import { settings } from '../../../settings';
|
||||
|
||||
/**
|
||||
* @summary Get a user preference
|
||||
* @param {String} userId The user ID
|
||||
* @param {String} preference The preference name
|
||||
* @param {unknown?} defaultValue The default value
|
||||
* @returns {unknown} The preference value
|
||||
*/
|
||||
export const getUserPreference = async (user: IUser | string, key: string, defaultValue: any = undefined) => {
|
||||
let preference;
|
||||
if (typeof user === 'string') {
|
||||
const dbUser = await Users.findOneById(user, { projection: { [`settings.preferences.${key}`]: 1 } });
|
||||
if (dbUser) {
|
||||
user = dbUser;
|
||||
}
|
||||
}
|
||||
if (typeof user === 'string') {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (user?.settings?.preferences?.hasOwnProperty(key)) {
|
||||
preference = user.settings.preferences[key];
|
||||
} else if (defaultValue === undefined) {
|
||||
preference = settings.get(`Accounts_Default_User_Preferences_${key}`);
|
||||
}
|
||||
|
||||
return preference !== undefined ? preference : defaultValue;
|
||||
};
|
||||
73
apps/meteor/server/lib/utils/lib/getValidRoomName.ts
Normal file
73
apps/meteor/server/lib/utils/lib/getValidRoomName.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Rooms } from '@rocket.chat/models';
|
||||
import { escapeHTML } from '@rocket.chat/tools';
|
||||
import limax from 'limax';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { settings } from '../../../settings';
|
||||
import { validateName } from '../../shared/validateName';
|
||||
|
||||
export const getValidRoomName = async (displayName: string, rid = '', options: { allowDuplicates?: boolean } = {}) => {
|
||||
let slugifiedName = displayName;
|
||||
|
||||
if (settings.get('UI_Allow_room_names_with_special_chars')) {
|
||||
const cleanName = limax(displayName, { maintainCase: true });
|
||||
if (options.allowDuplicates !== true) {
|
||||
const room = await Rooms.findOneByDisplayName(displayName);
|
||||
if (room && room._id !== rid) {
|
||||
if (room.archived) {
|
||||
throw new Meteor.Error('error-archived-duplicate-name', `There's an archived channel with name ${cleanName}`, {
|
||||
function: 'RocketChat.getValidRoomName',
|
||||
channel_name: cleanName,
|
||||
});
|
||||
} else {
|
||||
throw new Meteor.Error('error-duplicate-channel-name', `A channel with name '${cleanName}' exists`, {
|
||||
function: 'RocketChat.getValidRoomName',
|
||||
channel_name: cleanName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
slugifiedName = cleanName;
|
||||
}
|
||||
|
||||
let nameValidation;
|
||||
|
||||
try {
|
||||
nameValidation = new RegExp(`^${settings.get('UTF8_Channel_Names_Validation')}$`);
|
||||
} catch (error) {
|
||||
nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$');
|
||||
}
|
||||
|
||||
if (!nameValidation.test(slugifiedName) || !validateName(slugifiedName)) {
|
||||
throw new Meteor.Error('error-invalid-room-name', `${escapeHTML(slugifiedName)} is not a valid room name.`, {
|
||||
function: 'RocketChat.getValidRoomName',
|
||||
channel_name: escapeHTML(slugifiedName),
|
||||
});
|
||||
}
|
||||
|
||||
if (options.allowDuplicates !== true) {
|
||||
const room = await Rooms.findOneByName(slugifiedName);
|
||||
if (room && room._id !== rid) {
|
||||
if (settings.get('UI_Allow_room_names_with_special_chars')) {
|
||||
let tmpName = slugifiedName;
|
||||
let next = 0;
|
||||
while (await Rooms.findOneByNameAndNotId(tmpName, rid)) {
|
||||
tmpName = `${slugifiedName}-${++next}`;
|
||||
}
|
||||
slugifiedName = tmpName;
|
||||
} else if (room.archived) {
|
||||
throw new Meteor.Error('error-archived-duplicate-name', `There's an archived channel with name ${escapeHTML(slugifiedName)}`, {
|
||||
function: 'RocketChat.getValidRoomName',
|
||||
channel_name: escapeHTML(slugifiedName),
|
||||
});
|
||||
} else {
|
||||
throw new Meteor.Error('error-duplicate-channel-name', `A channel with name '${escapeHTML(slugifiedName)}' exists`, {
|
||||
function: 'RocketChat.getValidRoomName',
|
||||
channel_name: escapeHTML(slugifiedName),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return slugifiedName;
|
||||
};
|
||||
88
apps/meteor/server/lib/utils/lib/normalizeMessagesForUser.ts
Normal file
88
apps/meteor/server/lib/utils/lib/normalizeMessagesForUser.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { IMessage } from '@rocket.chat/core-typings';
|
||||
import { Users } from '@rocket.chat/models';
|
||||
|
||||
import { settings } from '../../../settings';
|
||||
|
||||
type NormalizableMessage = Pick<IMessage, 'u' | 'starred' | 'mentions' | 'reactions'>;
|
||||
|
||||
const filterStarred = <T extends Pick<IMessage, 'starred'>>(message: T, uid?: string): T => {
|
||||
// if Allow_anonymous_read is enabled, uid will be undefined
|
||||
if (!uid) return message;
|
||||
|
||||
// only return starred field if user has it starred
|
||||
if (message.starred && Array.isArray(message.starred)) {
|
||||
message.starred = message.starred.filter((star) => star._id === uid);
|
||||
}
|
||||
return message;
|
||||
};
|
||||
|
||||
// TODO: we should let clients get user names on demand instead of doing this
|
||||
|
||||
function getNameOfUsername(users: Map<string, string>, username: string): string {
|
||||
return users.get(username) || username;
|
||||
}
|
||||
|
||||
export const normalizeMessagesForUser = async <T extends NormalizableMessage = IMessage>(messages: T[], uid?: string): Promise<T[]> => {
|
||||
// if not using real names, there is nothing else to do
|
||||
if (!settings.get('UI_Use_Real_Name')) {
|
||||
return messages.map((message) => filterStarred(message, uid));
|
||||
}
|
||||
|
||||
const usernames: Set<string> = new Set();
|
||||
|
||||
messages.forEach((message) => {
|
||||
message = filterStarred(message, uid);
|
||||
|
||||
if (!message.u?.username) {
|
||||
return;
|
||||
}
|
||||
usernames.add(message.u.username);
|
||||
|
||||
(message.mentions || []).forEach(({ username }) => {
|
||||
if (username) {
|
||||
usernames.add(username);
|
||||
}
|
||||
});
|
||||
|
||||
Object.values(message.reactions || {}).forEach((reaction) => reaction.usernames.forEach((username) => usernames.add(username)));
|
||||
});
|
||||
|
||||
const names = new Map();
|
||||
|
||||
(
|
||||
await Users.findUsersByUsernames([...usernames.values()], {
|
||||
projection: {
|
||||
username: 1,
|
||||
name: 1,
|
||||
},
|
||||
}).toArray()
|
||||
).forEach((user) => {
|
||||
names.set(user.username, user.name);
|
||||
});
|
||||
|
||||
messages.forEach((message) => {
|
||||
if (!message.u) {
|
||||
return;
|
||||
}
|
||||
message.u.name = getNameOfUsername(names, message.u.username);
|
||||
|
||||
(message.mentions || []).forEach((mention) => {
|
||||
if (mention.username) {
|
||||
mention.name = getNameOfUsername(names, mention.username);
|
||||
}
|
||||
});
|
||||
|
||||
if (!message.reactions) {
|
||||
return messages;
|
||||
}
|
||||
|
||||
message.reactions = Object.fromEntries(
|
||||
Object.entries(message.reactions).map(([keys, reaction]) => {
|
||||
reaction.names = reaction.usernames.map((username) => getNameOfUsername(names, username));
|
||||
return [keys, reaction];
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return messages;
|
||||
};
|
||||
33
apps/meteor/server/lib/utils/lib/templateVarHandler.ts
Normal file
33
apps/meteor/server/lib/utils/lib/templateVarHandler.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
|
||||
const logger = new Logger('TemplateVarHandler');
|
||||
|
||||
export const templateVarHandler = function (variable: string, object: Record<string, any>): string | undefined {
|
||||
const templateRegex = /#{([\w\-]+)}/gi;
|
||||
let match = templateRegex.exec(variable);
|
||||
let tmpVariable = variable;
|
||||
|
||||
if (match == null) {
|
||||
if (!object.hasOwnProperty(variable)) {
|
||||
logger?.debug({ msg: 'User does not have attribute', attribute: variable });
|
||||
return;
|
||||
}
|
||||
return object[variable];
|
||||
}
|
||||
logger?.debug('template found. replacing values');
|
||||
while (match != null) {
|
||||
const tmplVar = match[0];
|
||||
const tmplAttrName = match[1];
|
||||
|
||||
if (!object.hasOwnProperty(tmplAttrName)) {
|
||||
logger?.debug({ msg: 'User does not have attribute', attribute: tmplAttrName });
|
||||
return;
|
||||
}
|
||||
|
||||
const attrVal = object[tmplAttrName];
|
||||
logger?.debug({ msg: 'Replacing template var', templateVar: tmplVar, value: attrVal });
|
||||
tmpVariable = tmpVariable.replace(tmplVar, attrVal);
|
||||
match = templateRegex.exec(variable);
|
||||
}
|
||||
return tmpVariable;
|
||||
};
|
||||
35
apps/meteor/server/lib/utils/placeholders.ts
Normal file
35
apps/meteor/server/lib/utils/placeholders.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { strLeft, strRightBack } from '../../../lib/utils/stringUtils';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
export const placeholders = {
|
||||
replace: (
|
||||
str: string,
|
||||
data: { name?: string; email?: string; password?: string; reason?: string; user?: string; room?: string; unsubscribe?: string },
|
||||
) => {
|
||||
if (!str) {
|
||||
return '';
|
||||
}
|
||||
|
||||
str = str.replace(/\[Site_Name\]/g, settings.get<string>('Site_Name') || '');
|
||||
str = str.replace(/\[Site_URL\]/g, settings.get<string>('Site_Url') || '');
|
||||
|
||||
if (data) {
|
||||
str = str.replace(/\[name\]/g, data.name || '');
|
||||
str = str.replace(/\[fname\]/g, strLeft(data.name, ' ') || '');
|
||||
str = str.replace(/\[lname\]/g, strRightBack(data.name, ' ') || '');
|
||||
str = str.replace(/\[email\]/g, data.email || '');
|
||||
str = str.replace(/\[password\]/g, data.password || '');
|
||||
str = str.replace(/\[reason\]/g, data.reason || '');
|
||||
str = str.replace(/\[User\]/g, data.user || '');
|
||||
str = str.replace(/\[Room\]/g, data.room || '');
|
||||
|
||||
if (data.unsubscribe) {
|
||||
str = str.replace(/\[unsubscribe\]/g, data.unsubscribe);
|
||||
}
|
||||
}
|
||||
|
||||
str = str.replace(/([^>\r\n]?)(\r\n|\n\r|\r|\n)/g, '$1<br>$2');
|
||||
|
||||
return str;
|
||||
},
|
||||
};
|
||||
9
apps/meteor/server/lib/utils/restrictions.ts
Normal file
9
apps/meteor/server/lib/utils/restrictions.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { fileUploadIsValidContentTypeFromSettings } from '../../../app/utils/lib/restrictions';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
export const fileUploadIsValidContentType = function (type: string | undefined, customWhiteList?: string): boolean {
|
||||
const blackList = settings.get<string>('FileUpload_MediaTypeBlackList');
|
||||
const whiteList = customWhiteList || settings.get<string>('FileUpload_MediaTypeWhiteList');
|
||||
|
||||
return fileUploadIsValidContentTypeFromSettings(type, whiteList, blackList);
|
||||
};
|
||||
166
apps/meteor/server/lib/utils/slashCommand.ts
Normal file
166
apps/meteor/server/lib/utils/slashCommand.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { MeteorError } from '@rocket.chat/core-services';
|
||||
import type {
|
||||
IMessage,
|
||||
SlashCommand,
|
||||
SlashCommandOptions,
|
||||
RequiredField,
|
||||
SlashCommandPreviewItem,
|
||||
SlashCommandPreviews,
|
||||
} from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { methodDeprecationLogger } from '../deprecationWarningLogger';
|
||||
|
||||
interface ISlashCommandAddParams<T extends string> {
|
||||
command: string;
|
||||
callback?: SlashCommand<T>['callback'];
|
||||
options?: SlashCommandOptions;
|
||||
result?: SlashCommand['result'];
|
||||
providesPreview?: boolean;
|
||||
previewer?: SlashCommand['previewer'];
|
||||
previewCallback?: SlashCommand['previewCallback'];
|
||||
appId?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export const slashCommands = {
|
||||
commands: {} as Record<string, SlashCommand>,
|
||||
add<T extends string>({
|
||||
command,
|
||||
callback,
|
||||
options = {},
|
||||
result,
|
||||
providesPreview = false,
|
||||
previewer,
|
||||
previewCallback,
|
||||
appId,
|
||||
description = '',
|
||||
}: ISlashCommandAddParams<T>): void {
|
||||
if (this.commands[command]) {
|
||||
return;
|
||||
}
|
||||
this.commands[command] = {
|
||||
command,
|
||||
callback,
|
||||
params: options.params,
|
||||
description: options.description || description,
|
||||
permission: options.permission,
|
||||
clientOnly: options.clientOnly || false,
|
||||
result,
|
||||
providesPreview: Boolean(providesPreview),
|
||||
previewer,
|
||||
previewCallback,
|
||||
appId,
|
||||
} as SlashCommand;
|
||||
},
|
||||
async run({
|
||||
command,
|
||||
message,
|
||||
params,
|
||||
triggerId,
|
||||
userId,
|
||||
}: {
|
||||
command: string;
|
||||
params: string;
|
||||
message: RequiredField<Partial<IMessage>, 'rid' | '_id'>;
|
||||
userId: string;
|
||||
triggerId?: string | undefined;
|
||||
}): Promise<unknown> {
|
||||
const cmd = this.commands[command];
|
||||
if (typeof cmd?.callback !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!message?.rid) {
|
||||
throw new MeteorError('invalid-command-usage', 'Executing a command requires at least a message with a room id.');
|
||||
}
|
||||
|
||||
return cmd.callback({ command, params, message, triggerId, userId });
|
||||
},
|
||||
async getPreviews(
|
||||
command: string,
|
||||
params: string,
|
||||
message: RequiredField<Partial<IMessage>, 'rid'>,
|
||||
userId: string,
|
||||
): Promise<SlashCommandPreviews | undefined> {
|
||||
const cmd = this.commands[command];
|
||||
if (typeof cmd?.previewer !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!message?.rid) {
|
||||
throw new MeteorError('invalid-command-usage', 'Executing a command requires at least a message with a room id.');
|
||||
}
|
||||
|
||||
const previewInfo = await cmd.previewer(command, params, message, userId);
|
||||
|
||||
if (!previewInfo?.items?.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A limit of ten results, to save time and bandwidth
|
||||
if (previewInfo.items.length >= 10) {
|
||||
previewInfo.items = previewInfo.items.slice(0, 10);
|
||||
}
|
||||
|
||||
return previewInfo;
|
||||
},
|
||||
async executePreview(
|
||||
command: string,
|
||||
params: string,
|
||||
message: Pick<IMessage, 'rid'> & Partial<Omit<IMessage, 'rid'>>,
|
||||
preview: SlashCommandPreviewItem,
|
||||
userId: string,
|
||||
triggerId?: string,
|
||||
) {
|
||||
const cmd = this.commands[command];
|
||||
if (typeof cmd?.previewCallback !== 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!message?.rid) {
|
||||
throw new MeteorError('invalid-command-usage', 'Executing a command requires at least a message with a room id.');
|
||||
}
|
||||
|
||||
// { id, type, value }
|
||||
if (!preview.id || !preview.type || !preview.value) {
|
||||
throw new MeteorError('error-invalid-preview', 'Preview Item must have an id, type, and value.');
|
||||
}
|
||||
|
||||
return cmd.previewCallback(command, params, message, preview, userId, triggerId);
|
||||
},
|
||||
};
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
slashCommand(params: { cmd: string; params: string; msg: IMessage; triggerId: string }): unknown;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async slashCommand(command) {
|
||||
methodDeprecationLogger.method('slashCommand', '9.0.0', '/v1/commands.run');
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'slashCommand',
|
||||
});
|
||||
}
|
||||
|
||||
if (!command?.cmd || !slashCommands.commands[command.cmd]) {
|
||||
throw new Meteor.Error('error-invalid-command', 'Invalid Command Provided', {
|
||||
method: 'executeSlashCommandPreview',
|
||||
});
|
||||
}
|
||||
|
||||
return slashCommands.run({
|
||||
command: command.cmd,
|
||||
params: command.params,
|
||||
message: command.msg,
|
||||
triggerId: command.triggerId,
|
||||
userId,
|
||||
});
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user